mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
Refactor layers rendering (#1120)
* feat: render states - use global fn * feat: render states - separate pole detection from layer render * feat: render provinces * chore: unify drawFillWithGap * refactor: drawIce * refactor: drawBorders * refactor: drawHeightmap * refactor: drawTemperature * refactor: drawBiomes * refactor: drawPrec * refactor: drawPrecipitation * refactor: drawPopulation * refactor: drawCells * refactor: geColor * refactor: drawMarkers * refactor: drawScaleBar * refactor: drawScaleBar * refactor: drawMilitary * refactor: pump version to 1.104.00 * refactor: pump version to 1.104.00 * refactor: drawCoastline and createDefaultRuler * refactor: drawCoastline * refactor: Features module start * refactor: features - define distance fields * feat: drawFeatures * feat: drawIce don't hide * feat: detect coastline - fix issue with border feature * feat: separate labels rendering from generation process * feat: auto-update and restore layers * refactor - change layers * refactor - sort layers * fix: regenerate burgs to re-render layers * fix: getColor is not defined * fix: burgs overview - don't auto-show labels on hover * fix: redraw population on change * refactor: improve tooltip logic for burg labels and icons * chore: pump version to 1.104.0 * fefactor: edit coastline and lake * fix: minot fixes * fix: submap --------- Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
parent
ec993d1a9b
commit
05de284e02
52 changed files with 2473 additions and 2713 deletions
120
modules/renderers/draw-borders.js
Normal file
120
modules/renderers/draw-borders.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"use strict";
|
||||
|
||||
function drawBorders() {
|
||||
TIME && console.time("drawBorders");
|
||||
const {cells, vertices} = pack;
|
||||
|
||||
const statePath = [];
|
||||
const provincePath = [];
|
||||
const checked = {};
|
||||
|
||||
const isLand = cellId => cells.h[cellId] >= 20;
|
||||
|
||||
for (let cellId = 0; cellId < cells.i.length; cellId++) {
|
||||
if (!cells.state[cellId]) continue;
|
||||
const provinceId = cells.province[cellId];
|
||||
const stateId = cells.state[cellId];
|
||||
|
||||
// bordering cell of another province
|
||||
if (provinceId) {
|
||||
const provToCell = cells.c[cellId].find(neibId => {
|
||||
const neibProvinceId = cells.province[neibId];
|
||||
return (
|
||||
neibProvinceId &&
|
||||
provinceId > neibProvinceId &&
|
||||
!checked[`prov-${provinceId}-${neibProvinceId}-${cellId}`] &&
|
||||
cells.state[neibId] === stateId
|
||||
);
|
||||
});
|
||||
|
||||
if (provToCell !== undefined) {
|
||||
const addToChecked = cellId => (checked[`prov-${provinceId}-${cells.province[provToCell]}-${cellId}`] = true);
|
||||
const border = getBorder({type: "province", fromCell: cellId, toCell: provToCell, addToChecked});
|
||||
|
||||
if (border) {
|
||||
provincePath.push(border);
|
||||
cellId--; // check the same cell again
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if cell is on state border
|
||||
const stateToCell = cells.c[cellId].find(neibId => {
|
||||
const neibStateId = cells.state[neibId];
|
||||
return isLand(neibId) && stateId > neibStateId && !checked[`state-${stateId}-${neibStateId}-${cellId}`];
|
||||
});
|
||||
|
||||
if (stateToCell !== undefined) {
|
||||
const addToChecked = cellId => (checked[`state-${stateId}-${cells.state[stateToCell]}-${cellId}`] = true);
|
||||
const border = getBorder({type: "state", fromCell: cellId, toCell: stateToCell, addToChecked});
|
||||
|
||||
if (border) {
|
||||
statePath.push(border);
|
||||
cellId--; // check the same cell again
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg.select("#borders").selectAll("path").remove();
|
||||
svg.select("#stateBorders").append("path").attr("d", statePath.join(" "));
|
||||
svg.select("#provinceBorders").append("path").attr("d", provincePath.join(" "));
|
||||
|
||||
function getBorder({type, fromCell, toCell, addToChecked}) {
|
||||
const getType = cellId => cells[type][cellId];
|
||||
const isTypeFrom = cellId => cellId < cells.i.length && getType(cellId) === getType(fromCell);
|
||||
const isTypeTo = cellId => cellId < cells.i.length && getType(cellId) === getType(toCell);
|
||||
|
||||
addToChecked(fromCell);
|
||||
const startingVertex = cells.v[fromCell].find(v => vertices.c[v].some(i => isLand(i) && isTypeTo(i)));
|
||||
if (startingVertex === undefined) return null;
|
||||
|
||||
const checkVertex = vertex =>
|
||||
vertices.c[vertex].some(isTypeFrom) && vertices.c[vertex].some(c => isLand(c) && isTypeTo(c));
|
||||
const chain = getVerticesLine({vertices, startingVertex, checkCell: isTypeFrom, checkVertex, addToChecked});
|
||||
if (chain.length > 1) return "M" + chain.map(cellId => vertices.p[cellId]).join(" ");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// connect vertices to chain to form a border
|
||||
function getVerticesLine({vertices, startingVertex, checkCell, checkVertex, addToChecked}) {
|
||||
let chain = []; // vertices chain to form a path
|
||||
let next = startingVertex;
|
||||
const MAX_ITERATIONS = vertices.c.length;
|
||||
|
||||
for (let run = 0; run < 2; run++) {
|
||||
// first run: from any vertex to a border edge
|
||||
// second run: from found border edge to another edge
|
||||
chain = [];
|
||||
|
||||
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||
const previous = chain.at(-1);
|
||||
const current = next;
|
||||
chain.push(current);
|
||||
|
||||
const neibCells = vertices.c[current];
|
||||
neibCells.map(addToChecked);
|
||||
|
||||
const [c1, c2, c3] = neibCells.map(checkCell);
|
||||
const [v1, v2, v3] = vertices.v[current].map(checkVertex);
|
||||
const [vertex1, vertex2, vertex3] = vertices.v[current];
|
||||
|
||||
if (v1 && vertex1 !== previous && c1 !== c2) next = vertex1;
|
||||
else if (v2 && vertex2 !== previous && c2 !== c3) next = vertex2;
|
||||
else if (v3 && vertex3 !== previous && c1 !== c3) next = vertex3;
|
||||
|
||||
if (next === current || next === startingVertex) {
|
||||
if (next === startingVertex) chain.push(startingVertex);
|
||||
startingVertex = next;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawBorders");
|
||||
}
|
||||
69
modules/renderers/draw-burg-icons.js
Normal file
69
modules/renderers/draw-burg-icons.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"use strict";
|
||||
|
||||
function drawBurgIcons() {
|
||||
TIME && console.time("drawBurgIcons");
|
||||
|
||||
icons.selectAll("circle, use").remove(); // cleanup
|
||||
|
||||
// capitals
|
||||
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
|
||||
const capitalIcons = burgIcons.select("#cities");
|
||||
const capitalSize = capitalIcons.attr("size") || 1;
|
||||
const capitalAnchors = anchors.selectAll("#cities");
|
||||
const capitalAnchorsSize = capitalAnchors.attr("size") || 2;
|
||||
|
||||
capitalIcons
|
||||
.selectAll("circle")
|
||||
.data(capitals)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "burg" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x)
|
||||
.attr("cy", d => d.y)
|
||||
.attr("r", capitalSize);
|
||||
|
||||
capitalAnchors
|
||||
.selectAll("use")
|
||||
.data(capitals.filter(c => c.port))
|
||||
.enter()
|
||||
.append("use")
|
||||
.attr("xlink:href", "#icon-anchor")
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - capitalAnchorsSize * 0.47, 2))
|
||||
.attr("y", d => rn(d.y - capitalAnchorsSize * 0.47, 2))
|
||||
.attr("width", capitalAnchorsSize)
|
||||
.attr("height", capitalAnchorsSize);
|
||||
|
||||
// towns
|
||||
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
|
||||
const townIcons = burgIcons.select("#towns");
|
||||
const townSize = townIcons.attr("size") || 0.5;
|
||||
const townsAnchors = anchors.selectAll("#towns");
|
||||
const townsAnchorsSize = townsAnchors.attr("size") || 1;
|
||||
|
||||
townIcons
|
||||
.selectAll("circle")
|
||||
.data(towns)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "burg" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x)
|
||||
.attr("cy", d => d.y)
|
||||
.attr("r", townSize);
|
||||
|
||||
townsAnchors
|
||||
.selectAll("use")
|
||||
.data(towns.filter(c => c.port))
|
||||
.enter()
|
||||
.append("use")
|
||||
.attr("xlink:href", "#icon-anchor")
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - townsAnchorsSize * 0.47, 2))
|
||||
.attr("y", d => rn(d.y - townsAnchorsSize * 0.47, 2))
|
||||
.attr("width", townsAnchorsSize)
|
||||
.attr("height", townsAnchorsSize);
|
||||
|
||||
TIME && console.timeEnd("drawBurgIcons");
|
||||
}
|
||||
39
modules/renderers/draw-burg-labels.js
Normal file
39
modules/renderers/draw-burg-labels.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
"use strict";
|
||||
|
||||
function drawBurgLabels() {
|
||||
TIME && console.time("drawBurgLabels");
|
||||
|
||||
burgLabels.selectAll("text").remove(); // cleanup
|
||||
|
||||
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
|
||||
const capitalSize = burgIcons.select("#cities").attr("size") || 1;
|
||||
burgLabels
|
||||
.select("#cities")
|
||||
.selectAll("text")
|
||||
.data(capitals)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("id", d => "burgLabel" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.attr("dy", `${capitalSize * -1.5}px`)
|
||||
.text(d => d.name);
|
||||
|
||||
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
|
||||
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
|
||||
burgLabels
|
||||
.select("#towns")
|
||||
.selectAll("text")
|
||||
.data(towns)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("id", d => "burgLabel" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.attr("dy", `${townSize * -2}px`)
|
||||
.text(d => d.name);
|
||||
|
||||
TIME && console.timeEnd("drawBurgLabels");
|
||||
}
|
||||
129
modules/renderers/draw-emblems.js
Normal file
129
modules/renderers/draw-emblems.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"use strict";
|
||||
|
||||
function drawEmblems() {
|
||||
TIME && console.time("drawEmblems");
|
||||
const {states, provinces, burgs} = pack;
|
||||
|
||||
const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coa.size !== 0);
|
||||
const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coa.size !== 0);
|
||||
const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coa.size !== 0);
|
||||
|
||||
const getStateEmblemsSize = () => {
|
||||
const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
|
||||
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
|
||||
const sizeMod = +emblems.select("#stateEmblems").attr("data-size") || 1;
|
||||
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
|
||||
};
|
||||
|
||||
const getProvinceEmblemsSize = () => {
|
||||
const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
|
||||
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
|
||||
const sizeMod = +emblems.select("#provinceEmblems").attr("data-size") || 1;
|
||||
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
|
||||
};
|
||||
|
||||
const getBurgEmblemSize = () => {
|
||||
const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
|
||||
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
|
||||
const sizeMod = +emblems.select("#burgEmblems").attr("data-size") || 1;
|
||||
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
|
||||
};
|
||||
|
||||
const sizeBurgs = getBurgEmblemSize();
|
||||
const burgCOAs = validBurgs.map(burg => {
|
||||
const {x, y} = burg;
|
||||
const size = burg.coa.size || 1;
|
||||
const shift = (sizeBurgs * size) / 2;
|
||||
return {type: "burg", i: burg.i, x: burg.coa.x || x, y: burg.coa.y || y, size, shift};
|
||||
});
|
||||
|
||||
const sizeProvinces = getProvinceEmblemsSize();
|
||||
const provinceCOAs = validProvinces.map(province => {
|
||||
const [x, y] = province.pole || pack.cells.p[province.center];
|
||||
const size = province.coa.size || 1;
|
||||
const shift = (sizeProvinces * size) / 2;
|
||||
return {type: "province", i: province.i, x: province.coa.x || x, y: province.coa.y || y, size, shift};
|
||||
});
|
||||
|
||||
const sizeStates = getStateEmblemsSize();
|
||||
const stateCOAs = validStates.map(state => {
|
||||
const [x, y] = state.pole || pack.cells.p[state.center];
|
||||
const size = state.coa.size || 1;
|
||||
const shift = (sizeStates * size) / 2;
|
||||
return {type: "state", i: state.i, x: state.coa.x || x, y: state.coa.y || y, size, shift};
|
||||
});
|
||||
|
||||
const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs);
|
||||
const simulation = d3
|
||||
.forceSimulation(nodes)
|
||||
.alphaMin(0.6)
|
||||
.alphaDecay(0.2)
|
||||
.velocityDecay(0.6)
|
||||
.force(
|
||||
"collision",
|
||||
d3.forceCollide().radius(d => d.shift)
|
||||
)
|
||||
.stop();
|
||||
|
||||
d3.timeout(function () {
|
||||
const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
|
||||
for (let i = 0; i < n; ++i) {
|
||||
simulation.tick();
|
||||
}
|
||||
|
||||
const burgNodes = nodes.filter(node => node.type === "burg");
|
||||
const burgString = burgNodes
|
||||
.map(
|
||||
d =>
|
||||
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
|
||||
d.size
|
||||
}em"/>`
|
||||
)
|
||||
.join("");
|
||||
emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString);
|
||||
|
||||
const provinceNodes = nodes.filter(node => node.type === "province");
|
||||
const provinceString = provinceNodes
|
||||
.map(
|
||||
d =>
|
||||
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
|
||||
d.size
|
||||
}em"/>`
|
||||
)
|
||||
.join("");
|
||||
emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString);
|
||||
|
||||
const stateNodes = nodes.filter(node => node.type === "state");
|
||||
const stateString = stateNodes
|
||||
.map(
|
||||
d =>
|
||||
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
|
||||
d.size
|
||||
}em"/>`
|
||||
)
|
||||
.join("");
|
||||
emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString);
|
||||
|
||||
invokeActiveZooming();
|
||||
});
|
||||
|
||||
TIME && console.timeEnd("drawEmblems");
|
||||
}
|
||||
|
||||
const getDataAndType = id => {
|
||||
if (id === "burgEmblems") return [pack.burgs, "burg"];
|
||||
if (id === "provinceEmblems") return [pack.provinces, "province"];
|
||||
if (id === "stateEmblems") return [pack.states, "state"];
|
||||
throw new Error(`Unknown emblem type: ${id}`);
|
||||
};
|
||||
|
||||
async function renderGroupCOAs(g) {
|
||||
const [data, type] = getDataAndType(g.id);
|
||||
|
||||
for (let use of g.children) {
|
||||
const i = +use.dataset.i;
|
||||
const id = type + "COA" + i;
|
||||
COArenderer.trigger(id, data[i].coa);
|
||||
use.setAttribute("href", "#" + id);
|
||||
}
|
||||
}
|
||||
61
modules/renderers/draw-features.js
Normal file
61
modules/renderers/draw-features.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"use strict";
|
||||
|
||||
function drawFeatures() {
|
||||
TIME && console.time("drawFeatures");
|
||||
const featurePaths = defs.select("#featurePaths");
|
||||
const landMask = defs.select("#land");
|
||||
const waterMask = defs.select("#water");
|
||||
|
||||
for (const feature of pack.features) {
|
||||
if (!feature || feature.type === "ocean") continue;
|
||||
|
||||
featurePaths
|
||||
.append("path")
|
||||
.attr("d", getFeaturePath(feature))
|
||||
.attr("id", "feature_" + feature.i)
|
||||
.attr("data-f", feature.i);
|
||||
|
||||
if (feature.type === "lake") {
|
||||
landMask
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i)
|
||||
.attr("fill", "black");
|
||||
lakes
|
||||
.select(`#${feature.group}`)
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i);
|
||||
} else {
|
||||
landMask
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i)
|
||||
.attr("fill", "white");
|
||||
waterMask
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i)
|
||||
.attr("fill", "black");
|
||||
const coastlineGroup = feature.group === "lake_island" ? "#lake_island" : "#sea_island";
|
||||
coastline
|
||||
.select(coastlineGroup)
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i);
|
||||
}
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawFeatures");
|
||||
}
|
||||
|
||||
function getFeaturePath(feature) {
|
||||
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
|
||||
const simplifiedPoints = simplify(points, 0.3);
|
||||
const clippedPoints = clipPoly(simplifiedPoints, 1);
|
||||
|
||||
const lineGen = d3.line().curve(d3.curveBasisClosed);
|
||||
const path = round(lineGen(clippedPoints));
|
||||
|
||||
return path;
|
||||
}
|
||||
144
modules/renderers/draw-heightmap.js
Normal file
144
modules/renderers/draw-heightmap.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"use strict";
|
||||
|
||||
function drawHeightmap() {
|
||||
TIME && console.time("drawHeightmap");
|
||||
|
||||
const ocean = terrs.select("#oceanHeights");
|
||||
const land = terrs.select("#landHeights");
|
||||
|
||||
ocean.selectAll("*").remove();
|
||||
land.selectAll("*").remove();
|
||||
|
||||
const paths = new Array(101);
|
||||
const {cells, vertices} = grid;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const heights = Array.from(cells.i).sort((a, b) => cells.h[a] - cells.h[b]);
|
||||
|
||||
// ocean cells
|
||||
const renderOceanCells = Boolean(+ocean.attr("data-render"));
|
||||
if (renderOceanCells) {
|
||||
const skip = +ocean.attr("skip") + 1 || 1;
|
||||
const relax = +ocean.attr("relax") || 0;
|
||||
lineGen.curve(d3[ocean.attr("curve") || "curveBasisClosed"]);
|
||||
|
||||
let currentLayer = 0;
|
||||
for (const i of heights) {
|
||||
const h = cells.h[i];
|
||||
if (h > currentLayer) currentLayer += skip;
|
||||
if (h < currentLayer) continue;
|
||||
if (currentLayer >= 20) break;
|
||||
if (used[i]) continue; // already marked
|
||||
const onborder = cells.c[i].some(n => cells.h[n] < h);
|
||||
if (!onborder) continue;
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
|
||||
const chain = connectVertices(cells, vertices, vertex, h, used);
|
||||
if (chain.length < 3) continue;
|
||||
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
|
||||
if (!paths[h]) paths[h] = "";
|
||||
paths[h] += round(lineGen(points));
|
||||
}
|
||||
}
|
||||
|
||||
// land cells
|
||||
{
|
||||
const skip = +land.attr("skip") + 1 || 1;
|
||||
const relax = +land.attr("relax") || 0;
|
||||
lineGen.curve(d3[land.attr("curve") || "curveBasisClosed"]);
|
||||
|
||||
let currentLayer = 20;
|
||||
for (const i of heights) {
|
||||
const h = cells.h[i];
|
||||
if (h > currentLayer) currentLayer += skip;
|
||||
if (h < currentLayer) continue;
|
||||
if (currentLayer > 100) break; // no layers possible with height > 100
|
||||
if (used[i]) continue; // already marked
|
||||
const onborder = cells.c[i].some(n => cells.h[n] < h);
|
||||
if (!onborder) continue;
|
||||
|
||||
const startVertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
|
||||
const chain = connectVertices(cells, vertices, startVertex, h, used);
|
||||
if (chain.length < 3) continue;
|
||||
|
||||
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
|
||||
if (!paths[h]) paths[h] = "";
|
||||
paths[h] += round(lineGen(points));
|
||||
}
|
||||
}
|
||||
|
||||
// render paths
|
||||
for (const height of d3.range(0, 101)) {
|
||||
const group = height < 20 ? ocean : land;
|
||||
const scheme = getColorScheme(group.attr("scheme"));
|
||||
|
||||
if (height === 0 && renderOceanCells) {
|
||||
// draw base ocean layer
|
||||
group
|
||||
.append("rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", graphWidth)
|
||||
.attr("height", graphHeight)
|
||||
.attr("fill", scheme(1));
|
||||
}
|
||||
|
||||
if (height === 20) {
|
||||
// draw base land layer
|
||||
group
|
||||
.append("rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", graphWidth)
|
||||
.attr("height", graphHeight)
|
||||
.attr("fill", scheme(0.8));
|
||||
}
|
||||
|
||||
if (paths[height] && paths[height].length >= 10) {
|
||||
const terracing = group.attr("terracing") / 10 || 0;
|
||||
const color = getColor(height, scheme);
|
||||
|
||||
if (terracing) {
|
||||
group
|
||||
.append("path")
|
||||
.attr("d", paths[height])
|
||||
.attr("transform", "translate(.7,1.4)")
|
||||
.attr("fill", d3.color(color).darker(terracing))
|
||||
.attr("data-height", height);
|
||||
}
|
||||
group.append("path").attr("d", paths[height]).attr("fill", color).attr("data-height", height);
|
||||
}
|
||||
}
|
||||
|
||||
// connect vertices to chain: specific case for heightmap
|
||||
function connectVertices(cells, vertices, start, h, used) {
|
||||
const MAX_ITERATIONS = vertices.c.length;
|
||||
|
||||
const n = cells.i.length;
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i = 0, current = start; i === 0 || (current !== start && i < MAX_ITERATIONS); i++) {
|
||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => cells.h[c] === h).forEach(c => (used[c] = 1));
|
||||
const c0 = c[0] >= n || cells.h[c[0]] < h;
|
||||
const c1 = c[1] >= n || cells.h[c[1]] < h;
|
||||
const c2 = c[2] >= n || cells.h[c[2]] < h;
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||
if (current === chain[chain.length - 1]) {
|
||||
ERROR && console.error("Next vertex is not found");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
function simplifyLine(chain, simplification) {
|
||||
if (!simplification) return chain;
|
||||
const n = simplification + 1; // filter each nth element
|
||||
return chain.filter((d, i) => i % n === 0);
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawHeightmap");
|
||||
}
|
||||
50
modules/renderers/draw-markers.js
Normal file
50
modules/renderers/draw-markers.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"use strict";
|
||||
|
||||
function drawMarkers() {
|
||||
TIME && console.time("drawMarkers");
|
||||
|
||||
const rescale = +markers.attr("rescale");
|
||||
const pinned = +markers.attr("pinned");
|
||||
|
||||
const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers;
|
||||
const html = markersData.map(marker => drawMarker(marker, rescale));
|
||||
markers.html(html.join(""));
|
||||
|
||||
TIME && console.timeEnd("drawMarkers");
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const pinShapes = {
|
||||
bubble: (fill, stroke) => `<path d="M6,19 l9,10 L24,19" fill="${stroke}" stroke="none" /><circle cx="15" cy="15" r="10" fill="${fill}" stroke="${stroke}"/>`,
|
||||
pin: (fill, stroke) => `<path d="m 15,3 c -5.5,0 -9.7,4.09 -9.7,9.3 0,6.8 9.7,17 9.7,17 0,0 9.7,-10.2 9.7,-17 C 24.7,7.09 20.5,3 15,3 Z" fill="${fill}" stroke="${stroke}"/>`,
|
||||
square: (fill, stroke) => `<path d="m 20,25 -5,4 -5,-4 z" fill="${stroke}"/><path d="M 5,5 H 25 V 25 H 5 Z" fill="${fill}" stroke="${stroke}"/>`,
|
||||
squarish: (fill, stroke) => `<path d="m 5,5 h 20 v 20 h -6 l -4,4 -4,-4 H 5 Z" fill="${fill}" stroke="${stroke}" />`,
|
||||
diamond: (fill, stroke) => `<path d="M 2,15 15,1 28,15 15,29 Z" fill="${fill}" stroke="${stroke}" />`,
|
||||
hex: (fill, stroke) => `<path d="M 15,29 4.61,21 V 9 L 15,3 25.4,9 v 12 z" fill="${fill}" stroke="${stroke}" />`,
|
||||
hexy: (fill, stroke) => `<path d="M 15,29 6,21 5,8 15,4 25,8 24,21 Z" fill="${fill}" stroke="${stroke}" />`,
|
||||
shieldy: (fill, stroke) => `<path d="M 15,29 6,21 5,7 c 0,0 5,-3 10,-3 5,0 10,3 10,3 l -1,14 z" fill="${fill}" stroke="${stroke}" />`,
|
||||
shield: (fill, stroke) => `<path d="M 4.6,5.2 H 25 v 6.7 A 20.3,20.4 0 0 1 15,29 20.3,20.4 0 0 1 4.6,11.9 Z" fill="${fill}" stroke="${stroke}" />`,
|
||||
pentagon: (fill, stroke) => `<path d="M 4,16 9,4 h 12 l 5,12 -11,13 z" fill="${fill}" stroke="${stroke}" />`,
|
||||
heptagon: (fill, stroke) => `<path d="M 15,29 6,22 4,12 10,4 h 10 l 6,8 -2,10 z" fill="${fill}" stroke="${stroke}" />`,
|
||||
circle: (fill, stroke) => `<circle cx="15" cy="15" r="11" fill="${fill}" stroke="${stroke}" />`,
|
||||
no: () => ""
|
||||
};
|
||||
|
||||
const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => {
|
||||
const shapeFunction = pinShapes[shape] || pinShapes.bubble;
|
||||
return shapeFunction(fill, stroke);
|
||||
};
|
||||
|
||||
function drawMarker(marker, rescale = 1) {
|
||||
const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker;
|
||||
const id = `marker${i}`;
|
||||
const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
|
||||
const viewX = rn(x - zoomSize / 2, 1);
|
||||
const viewY = rn(y - zoomSize, 1);
|
||||
|
||||
return /* html */ `
|
||||
<svg id="${id}" viewbox="0 0 30 30" width="${zoomSize}" height="${zoomSize}" x="${viewX}" y="${viewY}">
|
||||
<g>${getPin(pin, fill, stroke)}</g>
|
||||
<text x="${dx}%" y="${dy}%" font-size="${px}px" >${icon}</text>
|
||||
</svg>`;
|
||||
}
|
||||
126
modules/renderers/draw-military.js
Normal file
126
modules/renderers/draw-military.js
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"use strict";
|
||||
|
||||
function drawMilitary() {
|
||||
TIME && console.time("drawMilitary");
|
||||
|
||||
armies.selectAll("g").remove();
|
||||
pack.states.filter(s => s.i && !s.removed).forEach(s => drawRegiments(s.military, s.i));
|
||||
|
||||
TIME && console.timeEnd("drawMilitary");
|
||||
}
|
||||
|
||||
const drawRegiments = function (regiments, s) {
|
||||
const size = +armies.attr("box-size");
|
||||
const w = d => (d.n ? size * 4 : size * 6);
|
||||
const h = size * 2;
|
||||
const x = d => rn(d.x - w(d) / 2, 2);
|
||||
const y = d => rn(d.y - size, 2);
|
||||
|
||||
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
|
||||
const darkerColor = d3.color(baseColor).darker().hex();
|
||||
const army = armies
|
||||
.append("g")
|
||||
.attr("id", "army" + s)
|
||||
.attr("fill", baseColor)
|
||||
.attr("color", darkerColor);
|
||||
|
||||
const g = army
|
||||
.selectAll("g")
|
||||
.data(regiments)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("id", d => "regiment" + s + "-" + d.i)
|
||||
.attr("data-name", d => d.name)
|
||||
.attr("data-state", s)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("transform", d => (d.angle ? `rotate(${d.angle})` : null))
|
||||
.attr("transform-origin", d => `${d.x}px ${d.y}px`);
|
||||
g.append("rect")
|
||||
.attr("x", d => x(d))
|
||||
.attr("y", d => y(d))
|
||||
.attr("width", d => w(d))
|
||||
.attr("height", h);
|
||||
g.append("text")
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.text(d => Military.getTotal(d));
|
||||
g.append("rect")
|
||||
.attr("fill", "currentColor")
|
||||
.attr("x", d => x(d) - h)
|
||||
.attr("y", d => y(d))
|
||||
.attr("width", h)
|
||||
.attr("height", h);
|
||||
g.append("text")
|
||||
.attr("class", "regimentIcon")
|
||||
.attr("x", d => x(d) - size)
|
||||
.attr("y", d => d.y)
|
||||
.text(d => d.icon);
|
||||
};
|
||||
|
||||
const drawRegiment = function (reg, stateId) {
|
||||
const size = +armies.attr("box-size");
|
||||
const w = reg.n ? size * 4 : size * 6;
|
||||
const h = size * 2;
|
||||
const x1 = rn(reg.x - w / 2, 2);
|
||||
const y1 = rn(reg.y - size, 2);
|
||||
|
||||
let army = armies.select("g#army" + stateId);
|
||||
if (!army.size()) {
|
||||
const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999";
|
||||
const darkerColor = d3.color(baseColor).darker().hex();
|
||||
army = armies
|
||||
.append("g")
|
||||
.attr("id", "army" + stateId)
|
||||
.attr("fill", baseColor)
|
||||
.attr("color", darkerColor);
|
||||
}
|
||||
|
||||
const g = army
|
||||
.append("g")
|
||||
.attr("id", "regiment" + stateId + "-" + reg.i)
|
||||
.attr("data-name", reg.name)
|
||||
.attr("data-state", stateId)
|
||||
.attr("data-id", reg.i)
|
||||
.attr("transform", `rotate(${reg.angle || 0})`)
|
||||
.attr("transform-origin", `${reg.x}px ${reg.y}px`);
|
||||
g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
|
||||
g.append("text").attr("x", reg.x).attr("y", reg.y).text(getTotal(reg));
|
||||
g.append("rect")
|
||||
.attr("fill", "currentColor")
|
||||
.attr("x", x1 - h)
|
||||
.attr("y", y1)
|
||||
.attr("width", h)
|
||||
.attr("height", h);
|
||||
g.append("text")
|
||||
.attr("class", "regimentIcon")
|
||||
.attr("x", x1 - size)
|
||||
.attr("y", reg.y)
|
||||
.text(reg.icon);
|
||||
};
|
||||
|
||||
// move one regiment to another
|
||||
const moveRegiment = function (reg, x, y) {
|
||||
const el = armies.select("g#army" + reg.state).select("g#regiment" + reg.state + "-" + reg.i);
|
||||
if (!el.size()) return;
|
||||
|
||||
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
|
||||
reg.x = x;
|
||||
reg.y = y;
|
||||
const size = +armies.attr("box-size");
|
||||
const w = reg.n ? size * 4 : size * 6;
|
||||
const h = size * 2;
|
||||
const x1 = x => rn(x - w / 2, 2);
|
||||
const y1 = y => rn(y - size, 2);
|
||||
|
||||
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
|
||||
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
|
||||
el.select("text").transition(move).attr("x", x).attr("y", y);
|
||||
el.selectAll("rect:nth-of-type(2)")
|
||||
.transition(move)
|
||||
.attr("x", x1(x) - h)
|
||||
.attr("y", y1(y));
|
||||
el.select(".regimentIcon")
|
||||
.transition(move)
|
||||
.attr("x", x1(x) - size)
|
||||
.attr("y", y);
|
||||
};
|
||||
101
modules/renderers/draw-scalebar.js
Normal file
101
modules/renderers/draw-scalebar.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"use strict";
|
||||
|
||||
function drawScaleBar(scaleBar, scaleLevel) {
|
||||
if (!scaleBar.size() || scaleBar.style("display") === "none") return;
|
||||
|
||||
const unit = distanceUnitInput.value;
|
||||
const size = +scaleBar.attr("data-bar-size");
|
||||
|
||||
const length = getLength(scaleLevel, size);
|
||||
scaleBar.select("#scaleBarContent").remove(); // redraw content every time
|
||||
const content = scaleBar.append("g").attr("id", "scaleBarContent");
|
||||
|
||||
const lines = content.append("g");
|
||||
lines
|
||||
.append("line")
|
||||
.attr("x1", 0.5)
|
||||
.attr("y1", 0)
|
||||
.attr("x2", length + size - 0.5)
|
||||
.attr("y2", 0)
|
||||
.attr("stroke-width", size)
|
||||
.attr("stroke", "white");
|
||||
lines
|
||||
.append("line")
|
||||
.attr("x1", 0)
|
||||
.attr("y1", size)
|
||||
.attr("x2", length + size)
|
||||
.attr("y2", size)
|
||||
.attr("stroke-width", size)
|
||||
.attr("stroke", "#3d3d3d");
|
||||
lines
|
||||
.append("line")
|
||||
.attr("x1", 0)
|
||||
.attr("y1", 0)
|
||||
.attr("x2", length + size)
|
||||
.attr("y2", 0)
|
||||
.attr("stroke-width", rn(size * 3, 2))
|
||||
.attr("stroke-dasharray", size + " " + rn(length / 5 - size, 2))
|
||||
.attr("stroke", "#3d3d3d");
|
||||
|
||||
const texts = content.append("g").attr("text-anchor", "middle").attr("font-family", "var(--serif)");
|
||||
texts
|
||||
.selectAll("text")
|
||||
.data(d3.range(0, 6))
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("x", d => rn((d * length) / 5, 2))
|
||||
.attr("y", 0)
|
||||
.attr("dy", "-.6em")
|
||||
.text(d => rn((((d * length) / 5) * distanceScale) / scaleLevel) + (d < 5 ? "" : " " + unit));
|
||||
|
||||
const label = scaleBar.attr("data-label");
|
||||
if (label) {
|
||||
texts
|
||||
.append("text")
|
||||
.attr("x", (length + 1) / 2)
|
||||
.attr("dy", ".6em")
|
||||
.attr("dominant-baseline", "text-before-edge")
|
||||
.text(label);
|
||||
}
|
||||
|
||||
const scaleBarBack = scaleBar.select("#scaleBarBack");
|
||||
if (scaleBarBack.size()) {
|
||||
const bbox = content.node().getBBox();
|
||||
const paddingTop = +scaleBarBack.attr("data-top") || 0;
|
||||
const paddingLeft = +scaleBarBack.attr("data-left") || 0;
|
||||
const paddingRight = +scaleBarBack.attr("data-right") || 0;
|
||||
const paddingBottom = +scaleBarBack.attr("data-bottom") || 0;
|
||||
|
||||
scaleBar
|
||||
.select("#scaleBarBack")
|
||||
.attr("x", -paddingLeft)
|
||||
.attr("y", -paddingTop)
|
||||
.attr("width", bbox.width + paddingRight)
|
||||
.attr("height", bbox.height + paddingBottom);
|
||||
}
|
||||
}
|
||||
|
||||
function getLength(scaleLevel) {
|
||||
const init = 100;
|
||||
|
||||
const size = +scaleBar.attr("data-bar-size");
|
||||
let val = (init * size * distanceScale) / scaleLevel; // bar length in distance unit
|
||||
if (val > 900) val = rn(val, -3); // round to 1000
|
||||
else if (val > 90) val = rn(val, -2); // round to 100
|
||||
else if (val > 9) val = rn(val, -1); // round to 10
|
||||
else val = rn(val); // round to 1
|
||||
const length = (val * scaleLevel) / distanceScale; // actual length in pixels on this scale
|
||||
return length;
|
||||
}
|
||||
|
||||
function fitScaleBar(scaleBar, fullWidth, fullHeight) {
|
||||
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
|
||||
|
||||
const posX = +scaleBar.attr("data-x") || 99;
|
||||
const posY = +scaleBar.attr("data-y") || 99;
|
||||
const bbox = scaleBar.select("rect").node().getBBox();
|
||||
|
||||
const x = rn((fullWidth * posX) / 100 - bbox.width + 10);
|
||||
const y = rn((fullHeight * posY) / 100 - bbox.height + 20);
|
||||
scaleBar.attr("transform", `translate(${x},${y})`);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
// list - an optional array of stateIds to regenerate
|
||||
function drawStateLabels(list) {
|
||||
console.time("drawStateLabels");
|
||||
TIME && console.time("drawStateLabels");
|
||||
|
||||
// temporary make the labels visible
|
||||
const layerDisplay = labels.style("display");
|
||||
|
|
@ -289,5 +289,5 @@ function drawStateLabels(list) {
|
|||
return false;
|
||||
}
|
||||
|
||||
console.timeEnd("drawStateLabels");
|
||||
TIME && console.timeEnd("drawStateLabels");
|
||||
}
|
||||
104
modules/renderers/draw-temperature.js
Normal file
104
modules/renderers/draw-temperature.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"use strict";
|
||||
|
||||
function drawTemperature() {
|
||||
TIME && console.time("drawTemperature");
|
||||
|
||||
temperature.selectAll("*").remove();
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
const scheme = d3.scaleSequential(d3.interpolateSpectral);
|
||||
|
||||
const tMax = +byId("temperatureEquatorOutput").max;
|
||||
const tMin = +byId("temperatureEquatorOutput").min;
|
||||
const delta = tMax - tMin;
|
||||
|
||||
const {cells, vertices} = grid;
|
||||
const n = cells.i.length;
|
||||
|
||||
const checkedCells = new Uint8Array(n);
|
||||
const addToChecked = cellId => (checkedCells[cellId] = 1);
|
||||
|
||||
const min = d3.min(cells.temp);
|
||||
const max = d3.max(cells.temp);
|
||||
const step = Math.max(Math.round(Math.abs(min - max) / 5), 1);
|
||||
|
||||
const isolines = d3.range(min + step, max, step);
|
||||
const chains = [];
|
||||
const labels = []; // store label coordinates
|
||||
|
||||
for (const cellId of cells.i) {
|
||||
const t = cells.temp[cellId];
|
||||
if (checkedCells[cellId] || !isolines.includes(t)) continue;
|
||||
|
||||
const startingVertex = findStart(cellId, t);
|
||||
if (!startingVertex) continue;
|
||||
checkedCells[cellId] = 1;
|
||||
|
||||
const ofSameType = cellId => cells.temp[cellId] >= t;
|
||||
const chain = connectVertices({vertices, startingVertex, ofSameType, addToChecked});
|
||||
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
|
||||
if (relaxed.length < 6) continue;
|
||||
|
||||
const points = relaxed.map(v => vertices.p[v]);
|
||||
chains.push([t, points]);
|
||||
addLabel(points, t);
|
||||
}
|
||||
|
||||
// min temp isoline covers all graph
|
||||
temperature
|
||||
.append("path")
|
||||
.attr("d", `M0,0 h${graphWidth} v${graphHeight} h${-graphWidth} Z`)
|
||||
.attr("fill", scheme(1 - (min - tMin) / delta))
|
||||
.attr("stroke", "none");
|
||||
|
||||
for (const t of isolines) {
|
||||
const path = chains
|
||||
.filter(c => c[0] === t)
|
||||
.map(c => round(lineGen(c[1])))
|
||||
.join("");
|
||||
if (!path) continue;
|
||||
const fill = scheme(1 - (t - tMin) / delta),
|
||||
stroke = d3.color(fill).darker(0.2);
|
||||
temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
|
||||
}
|
||||
|
||||
const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1);
|
||||
tempLabels
|
||||
.selectAll("text")
|
||||
.data(labels)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("x", d => d[0])
|
||||
.attr("y", d => d[1])
|
||||
.text(d => convertTemperature(d[2]));
|
||||
|
||||
// find cell with temp < isotherm and find vertex to start path detection
|
||||
function findStart(i, t) {
|
||||
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
|
||||
return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])];
|
||||
}
|
||||
|
||||
function addLabel(points, t) {
|
||||
const xCenter = svgWidth / 2;
|
||||
|
||||
// add label on isoline top center
|
||||
const tc =
|
||||
points[d3.scan(points, (a, b) => a[1] - b[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
|
||||
pushLabel(tc[0], tc[1], t);
|
||||
|
||||
// add label on isoline bottom center
|
||||
if (points.length > 20) {
|
||||
const bc =
|
||||
points[d3.scan(points, (a, b) => b[1] - a[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
|
||||
const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point
|
||||
if (dist2 > 100) pushLabel(bc[0], bc[1], t);
|
||||
}
|
||||
}
|
||||
|
||||
function pushLabel(x, y, t) {
|
||||
if (x < 20 || x > svgWidth - 20) return;
|
||||
if (y < 20 || y > svgHeight - 20) return;
|
||||
labels.push([x, y, t]);
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawTemperature");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue