Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into burg-groups

This commit is contained in:
Azgaar 2025-03-15 14:49:43 +01:00
commit 95b7ed9ea4
33 changed files with 572 additions and 378 deletions

View file

@ -172,6 +172,7 @@ t,
#texture,
#landmass,
#vignette,
#gridOverlay,
#fogging {
pointer-events: none;
}

View file

@ -11,8 +11,8 @@
name="description"
content="Free web app that helps fantasy writers, game masters, and cartographers create and edit fantasy maps"
/>
<meta property="og:url" content="https://azgaar.github.io/Fantasy-Map-Generator" />
<meta property="og:title" content="Azgaar's Fantasy Map Generator" />
<meta
property="og:description"
@ -138,7 +138,7 @@
}
</style>
<link rel="preload" href="index.css?v=1.106.3" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="index.css?v=1.108.6" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link
rel="preload"
@ -354,9 +354,7 @@
<g id="statePaths"></g>
<g id="defs-emblems"></g>
<mask id="land"></mask>
<mask id="water">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
</mask>
<mask id="water"></mask>
<mask id="fog" style="stroke-width: 10; stroke: black; stroke-linejoin: round; stroke-opacity: 0.1">
<rect x="0" y="0" width="100%" height="100%" fill="white" stroke="none" />
</mask>
@ -3575,9 +3573,9 @@
<input id="markerType" style="width: 10.3em" />
</div>
<div data-tip="Marker icon. Paste any Unicode symbol or select from the predefined list">
<div data-tip="Marker icon" style="display: flex; align-items: center">
<div class="label">Icon:</div>
<input id="markerIcon" style="width: 5em" />
<div id="markerIcon" style="font-size: 1.5em; width: 3.7em">👑</div>
<button id="markerIconSelect" style="width: 5em">select</button>
</div>
@ -3666,10 +3664,10 @@
></i>
</div>
<div data-tip="Regiment emblem. Paste any Unicode symbol or select from the predefined list">
<div class="label italic">Emblem:</div>
<input id="regimentEmblem" style="width: 5em" />
<button id="regimentEmblemSelect" style="padding: 0; width: 4.5em">select</button>
<div data-tip="Regiment emblem" style="display: flex; align-items: center">
<div class="label">Emblem:</div>
<div id="regimentEmblem" style="font-size: 1.5em; width: 3.7em"></div>
<button id="regimentEmblemChange" style="padding: 0; width: 4.5em">change</button>
</div>
<div id="regimentComposition" class="table"></div>
@ -5823,11 +5821,24 @@
</div>
<div id="iconSelector" style="display: none" class="dialog">
<table id="iconTable" class="table pointer" style="font-size: 2em; text-align: center; width: 100%"></table>
<div style="font-style: italic; font-size: 1.2em; margin: 0.4em 0 0 0.4em">
<div>
<b>Unicode emojis</b>
<div style="font-style: italic">
<span>Select from the list or paste a Unicode character here: </span>
<input id="iconInput" style="width: 2.5em" />
<span>. See <a href="https://emojipedia.org" target="_blank">Emojipedia</a> for reference</span>
<span>. See <a href="https://emojidb.org" target="_blank">EmojiDB</a> to search for emojis</span>
</div>
<table id="iconTable" class="table pointer" style="font-size: 2em; text-align: center; width: 100%"></table>
</div>
<div style="margin-top: 0.5em">
<b>External images</b>
<div style="font-style: italic">
<span>Paste link to the image here: </span>
<input id="imageInput" style="width: 20em" />
<button id="addImage" type="button">Add</button>
</div>
<div id="addedIcons" class="pointer" style="display: flex; flex-wrap: wrap; max-width: 420px"></div>
</div>
</div>
@ -8312,7 +8323,7 @@
<script src="config/precreated-heightmaps.js"></script>
<script src="modules/heightmap-generator.js?v=1.99.00"></script>
<script src="modules/features.js?v=1.104.0"></script>
<script src="modules/ocean-layers.js?v=1.104.8"></script>
<script src="modules/ocean-layers.js?v=1.108.4"></script>
<script src="modules/river-generator.js?v=1.106.7"></script>
<script src="modules/lakes.js?v=1.99.00"></script>
<script src="modules/biomes.js?v=1.99.00"></script>
@ -8323,8 +8334,8 @@
<script src="modules/provinces-generator.js?v=1.106.0"></script>
<script src="modules/routes-generator.js?v=1.106.0"></script>
<script src="modules/religions-generator.js?v=1.106.0"></script>
<script src="modules/military-generator.js?v=1.104.0"></script>
<script src="modules/markers-generator.js?v=1.104.0"></script>
<script src="modules/military-generator.js?v=1.107.0"></script>
<script src="modules/markers-generator.js?v=1.107.0"></script>
<script src="modules/zones-generator.js?v=1.106.0"></script>
<script src="modules/coa-generator.js?v=1.99.00"></script>
<script src="modules/resample.js?v=1.106.4"></script>
@ -8333,21 +8344,20 @@
<script src="libs/lineclip.min.js?v1.105.0"></script>
<script src="libs/simplify.js?v1.105.6"></script>
<script src="modules/fonts.js?v=1.99.03"></script>
<script src="modules/ui/layers.js?v=1.106.0"></script>
<script src="modules/ui/layers.js?v=1.108.4"></script>
<script src="modules/ui/measurers.js?v=1.99.00"></script>
<script src="modules/ui/style-presets.js?v=1.100.00"></script>
<script src="modules/ui/general.js?v=1.100.00"></script>
<script src="modules/ui/options.js?v=1.106.2"></script>
<script src="main.js?v=1.106.0"></script>
<script src="main.js?v=1.108.1"></script>
<script defer src="modules/relief-icons.js?v=1.99.05"></script>
<script defer src="modules/ui/style.js?v=1.104.0"></script>
<script defer src="modules/ui/editors.js?v=1.106.1"></script>
<script defer src="modules/ui/tools.js?v=1.106.0"></script>
<script defer src="modules/ui/style.js?v=1.108.4"></script>
<script defer src="modules/ui/editors.js?v=1.108.5"></script>
<script defer src="modules/ui/tools.js?v=1.108.5"></script>
<script defer src="modules/ui/world-configurator.js?v=1.105.4"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.105.2"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.106.1"></script>
<script defer src="modules/ui/biomes-editor.js?v=1.99.05"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.108.1"></script>
<script defer src="modules/ui/biomes-editor.js?v=1.108.4"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.105.11"></script>
<script defer src="modules/ui/elevation-profile.js?v=1.99.00"></script>
<script defer src="modules/ui/temperature-graph.js?v=1.106.6"></script>
@ -8364,20 +8374,20 @@
<script defer src="modules/ui/burg-group-editor.js?v=1.106.0"></script>
<script defer src="modules/ui/burg-editor.js?v=1.106.6"></script>
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
<script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
<script defer src="modules/ui/notes-editor.js?v=1.107.3"></script>
<script defer src="modules/ui/ai-generator.js?v=1.105.22"></script>
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/zones-editor.js?v=1.105.20"></script>
<script defer src="modules/ui/burgs-overview.js?v=1.105.15"></script>
<script defer src="modules/ui/routes-overview.js?v=1.104.3"></script>
<script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/military-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/regiments-overview.js?v=1.104.0"></script>
<script defer src="modules/ui/markers-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/regiment-editor.js?v=1.104.14"></script>
<script defer src="modules/ui/battle-screen.js?v=1.99.00"></script>
<script defer src="modules/ui/military-overview.js?v=1.108.5"></script>
<script defer src="modules/ui/regiments-overview.js?v=1.108.5"></script>
<script defer src="modules/ui/markers-overview.js?v=1.108.5"></script>
<script defer src="modules/ui/regiment-editor.js?v=1.108.5"></script>
<script defer src="modules/ui/battle-screen.js?v=1.108.5"></script>
<script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/markers-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/markers-editor.js?v=1.108.5"></script>
<script defer src="modules/ui/3d.js?v=1.99.00"></script>
<script defer src="modules/ui/submap-tool.js?v=1.106.2"></script>
<script defer src="modules/ui/transform-tool.js?v=1.106.2"></script>
@ -8385,21 +8395,22 @@
<script defer src="modules/coa-renderer.js?v=1.99.00"></script>
<script defer src="libs/rgbquant.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
<script defer src="modules/io/save.js?v=1.100.00"></script>
<script defer src="modules/io/load.js?v=1.105.24"></script>
<script defer src="modules/io/save.js?v=1.107.4"></script>
<script defer src="modules/io/load.js?v=1.108.0"></script>
<script defer src="modules/io/cloud.js?v=1.106.0"></script>
<script defer src="modules/io/export.js?v=1.100.00"></script>
<script defer src="modules/renderers/draw-features.js?v=1.106.0"></script>
<script defer src="modules/renderers/draw-features.js?v=1.108.2"></script>
<script defer src="modules/renderers/draw-borders.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-heightmap.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-markers.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-scalebar.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-markers.js?v=1.108.5"></script>
<script defer src="modules/renderers/draw-scalebar.js?v=1.108.1"></script>
<script defer src="modules/renderers/draw-temperature.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-emblems.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-military.js?v=1.104.13"></script>
<script defer src="modules/renderers/draw-state-labels.js?v=1.106.0"></script>
<script defer src="modules/renderers/draw-burg-labels.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-military.js?v=1.108.5"></script>
<script defer src="modules/renderers/draw-state-labels.js?v=1.108.1"></script>
<script defer src="modules/renderers/draw-burg-labels.js?v=1.108.1"></script>
<script defer src="modules/renderers/draw-burg-icons.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-relief-icons.js?v=1.108.4"></script>
</body>
</html>

View file

@ -1071,7 +1071,7 @@ function generatePrecipitation() {
const from = west[0][0],
to = west[west.length - 1][0];
const y = (grid.points[from][1] + grid.points[to][1]) / 2;
wind.append("text").attr("x", 20).attr("y", y).text("\u21C9");
wind.append("text").attr("text-rendering", "optimizeSpeed").attr("x", 20).attr("y", y).text("\u21C9");
}
}
if (easterly.length > 1) {
@ -1082,6 +1082,7 @@ function generatePrecipitation() {
const y = (grid.points[from][1] + grid.points[to][1]) / 2;
wind
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", graphWidth - 52)
.attr("y", y)
.text("\u21C7");
@ -1092,12 +1093,14 @@ function generatePrecipitation() {
if (northerly)
wind
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", graphWidth / 2)
.attr("y", 42)
.text("\u21CA");
if (southerly)
wind
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", graphWidth / 2)
.attr("y", graphHeight - 20)
.text("\u21C8");

View file

@ -941,7 +941,6 @@ export function resolveVersionConflicts(mapVersion) {
defs.select("#land").selectAll("path, use").remove();
defs.select("#water").selectAll("path, use").remove();
viewbox.select("#coastline").selectAll("path, use").remove();
drawFeatures();
// v1.104.0 introduced bugs with state borders
regions
@ -957,6 +956,24 @@ export function resolveVersionConflicts(mapVersion) {
}
if (isOlderThan("1.107.0")) {
// v1.107.0 allowed custom images for markers and regiments
if (layerIsOn("toggleMarkers")) drawMarkers();
if (layerIsOn("toggleMilitary")) drawMilitary();
}
if (isOlderThan("1.108.0")) {
// v1.108.0 changed features rendering method
pack.features.forEach(f => {
// fix lakes with missing group
if (f?.type === "lake" && !f.group) f.group = "freshwater";
});
drawFeatures();
// some old maps has incorrect "heights" groups
viewbox.selectAll("#heights").remove();
}
if (isOlderThan("1.109.0")) {
// v1.107.0 changeв burg groups and added customizable icons
icons.selectAll("circle, use").remove();

View file

@ -743,6 +743,7 @@ function showStatesChart() {
node
.append("text")
.attr("text-rendering", "optimizeSpeed")
.style("font-size", d => rn((d.r ** 0.97 * 4) / lp(d.data.name), 2) + "px")
.selectAll("tspan")
.data(d => d.data.name.split(exp))

View file

@ -257,6 +257,7 @@ async function parseLoadedData(data, mapVersion) {
if (settings[23]) rescaleLabels.checked = +settings[23];
if (settings[24]) urbanDensity = urbanDensityInput.value = +settings[24];
if (settings[25]) longitudeInput.value = longitudeOutput.value = minmax(settings[25] || 50, 0, 100);
if (settings[26]) growthRate.value = settings[26];
}
{
@ -471,7 +472,7 @@ async function parseLoadedData(data, mapVersion) {
{
// dynamically import and run auto-update script
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.105.24");
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.108.0");
resolveVersionConflicts(mapVersion);
}
@ -489,12 +490,16 @@ async function parseLoadedData(data, mapVersion) {
if (textureHref) updateTextureSelectValue(textureHref);
}
// data integrity checks
{
const cells = pack.cells;
const {cells, vertices} = pack;
if (pack.cells.i.length !== pack.cells.state.length) {
const message = "[Data integrity] Striping issue detected. To fix edit the heightmap in ERASE mode";
ERROR && console.error(message);
const cellsMismatch = cells.i.length !== cells.state.length;
const featureVerticesMismatch = pack.features.some(f => f?.vertices?.some(vertex => !vertices.p[vertex]));
if (cellsMismatch || featureVerticesMismatch) {
const message = "[Data integrity] Striping issue detected. To fix try to edit the heightmap in ERASE mode";
throw new Error(message);
}
const invalidStates = [...new Set(cells.state)].filter(s => !pack.states[s] || pack.states[s].removed);
@ -745,7 +750,7 @@ async function parseLoadedData(data, mapVersion) {
$("#alert").dialog({
resizable: false,
title: "Loading error",
maxWidth: "50em",
maxWidth: "40em",
buttons: {
"Clear cache": () => cleanupData(),
"Select file": function () {

View file

@ -68,7 +68,8 @@ function prepareMapData() {
stylePreset.value,
+rescaleLabels.checked,
urbanDensity,
longitudeOutput.value
longitudeOutput.value,
growthRate.value
].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");

View file

@ -11,7 +11,7 @@ window.Markers = (function () {
/*
Default markers config:
type - short description (snake-case)
icon - unicode character, make sure it's supported by most of the browsers. Source: emojipedia.org
icon - unicode character or url to image
dx: icon offset in x direction, in pixels
dy: icon offset in y direction, in pixels
min: minimum number of candidates to add at least 1 marker

View file

@ -380,7 +380,7 @@ window.Military = (function () {
: gauss(options.year - 100, 150, 1, options.year - 6);
const conflict = campaign ? ` during the ${campaign.name}` : "";
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend});
notes.push({id: `regiment${s.i}-${r.i}`, name: r.name, legend});
};
return {

View file

@ -1,128 +0,0 @@
"use strict";
window.ReliefIcons = (function () {
const draw = function () {
TIME && console.time("drawRelief");
terrain.selectAll("*").remove();
const cells = pack.cells;
const density = terrain.attr("density") || 0.4;
const size = 2 * (terrain.attr("size") || 1);
const mod = 0.2 * size; // size modifier
const relief = [];
for (const i of cells.i) {
const height = cells.h[i];
if (height < 20) continue; // no icons on water
if (cells.r[i]) continue; // no icons on rivers
const biome = cells.biome[i];
if (height < 50 && biomesData.iconsDensity[biome] === 0) continue; // no icons for this biome
const polygon = getPackPolygon(i);
const [minX, maxX] = d3.extent(polygon, p => p[0]);
const [minY, maxY] = d3.extent(polygon, p => p[1]);
if (height < 50) placeBiomeIcons(i, biome);
else placeReliefIcons(i);
function placeBiomeIcons() {
const iconsDensity = biomesData.iconsDensity[biome] / 100;
const radius = 2 / iconsDensity / density;
if (Math.random() > iconsDensity * 10) return;
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
let h = (4 + Math.random()) * size;
const icon = getBiomeIcon(i, biomesData.icons[biome]);
if (icon === "#relief-grass-1") h *= 1.2;
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
function placeReliefIcons(i) {
const radius = 2 / density;
const [icon, h] = getReliefIcon(i, height);
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
function getReliefIcon(i, h) {
const temp = grid.cells.temp[pack.cells.g[i]];
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
const size = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6);
return [getIcon(type), size];
}
}
// sort relief icons by y+size
relief.sort((a, b) => a.y + a.s - (b.y + b.s));
let reliefHTML = "";
for (const r of relief) {
reliefHTML += `<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`;
}
terrain.html(reliefHTML);
TIME && console.timeEnd("drawRelief");
};
function getBiomeIcon(i, b) {
let type = b[Math.floor(Math.random() * b.length)];
const temp = grid.cells.temp[pack.cells.g[i]];
if (type === "conifer" && temp < 0) type = "coniferSnow";
return getIcon(type);
}
function getVariant(type) {
switch (type) {
case "mount":
return rand(2, 7);
case "mountSnow":
return rand(1, 6);
case "hill":
return rand(2, 5);
case "conifer":
return 2;
case "coniferSnow":
return 1;
case "swamp":
return rand(2, 3);
case "cactus":
return rand(1, 3);
case "deadTree":
return rand(1, 2);
default:
return 2;
}
}
function getOldIcon(type) {
switch (type) {
case "mountSnow":
return "mount";
case "vulcan":
return "mount";
case "coniferSnow":
return "conifer";
case "cactus":
return "dune";
case "deadTree":
return "dune";
default:
return type;
}
}
function getIcon(type) {
const set = terrain.attr("set") || "simple";
if (set === "simple") return "#relief-" + getOldIcon(type) + "-1";
if (set === "colored") return "#relief-" + type + "-" + getVariant(type);
if (set === "gray") return "#relief-" + type + "-" + getVariant(type) + "-bw";
return "#relief-" + getOldIcon(type) + "-1"; // simple
}
return {draw};
})();

View file

@ -19,6 +19,7 @@ function drawBurgLabels() {
.data(burgsInGroup)
.enter()
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
@ -38,6 +39,7 @@ function drawBurgLabel(burg) {
group
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", "burgLabel" + burg.i)
.attr("data-id", burg.i)
.attr("x", burg.x)

View file

@ -2,55 +2,60 @@
function drawFeatures() {
TIME && console.time("drawFeatures");
const featurePaths = defs.select("#featurePaths");
const landMask = defs.select("#land");
const waterMask = defs.select("#water");
const html = {
paths: [],
landMask: [],
waterMask: ['<rect x="0" y="0" width="100%" height="100%" fill="white" />'],
coastline: {},
lakes: {}
};
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);
html.paths.push(`<path d="${getFeaturePath(feature)}" id="feature_${feature.i}" data-f="${feature.i}"></path>`);
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);
html.landMask.push(`<use href="#feature_${feature.i}" data-f="${feature.i}" fill="black"></use>`);
const lakeGroup = feature.group || "freshwater";
if (!html.lakes[lakeGroup]) html.lakes[lakeGroup] = [];
html.lakes[lakeGroup].push(`<use href="#feature_${feature.i}" data-f="${feature.i}"></use>`);
} 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);
html.landMask.push(`<use href="#feature_${feature.i}" data-f="${feature.i}" fill="white"></use>`);
html.waterMask.push(`<use href="#feature_${feature.i}" data-f="${feature.i}" fill="black"></use>`);
const coastlineGroup = feature.group === "lake_island" ? "lake_island" : "sea_island";
if (!html.coastline[coastlineGroup]) html.coastline[coastlineGroup] = [];
html.coastline[coastlineGroup].push(`<use href="#feature_${feature.i}" data-f="${feature.i}"></use>`);
}
}
defs.select("#featurePaths").html(html.paths.join(""));
defs.select("#land").html(html.landMask.join(""));
defs.select("#water").html(html.waterMask.join(""));
coastline.selectAll("g").each(function () {
const paths = html.coastline[this.id] || [];
d3.select(this).html(paths.join(""));
});
lakes.selectAll("g").each(function () {
const paths = html.lakes[this.id] || [];
d3.select(this).html(paths.join(""));
});
TIME && console.timeEnd("drawFeatures");
}
function getFeaturePath(feature) {
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
if (points.some(point => point === undefined)) {
ERROR && console.error("Undefined point in getFeaturePath");
return "";
}
const simplifiedPoints = simplify(points, 0.3);
const clippedPoints = clipPoly(simplifiedPoints, 1);

View file

@ -42,9 +42,12 @@ function drawMarker(marker, rescale = 1) {
const viewX = rn(x - zoomSize / 2, 1);
const viewY = rn(y - zoomSize, 1);
const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
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>
<text x="${dx}%" y="${dy}%" font-size="${px}px" >${isExternal ? "" : icon}</text>
<image x="${dx / 2}%" y="${dy / 2}%" width="${px}px" height="${px}px" href="${isExternal ? icon : ""}" />
</svg>`;
}

View file

@ -43,6 +43,7 @@ const drawRegiments = function (regiments, s) {
g.append("text")
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("text-rendering", "optimizeSpeed")
.text(d => Military.getTotal(d));
g.append("rect")
.attr("fill", "currentColor")
@ -52,9 +53,17 @@ const drawRegiments = function (regiments, s) {
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("text-rendering", "optimizeSpeed")
.attr("x", d => x(d) - size)
.attr("y", d => d.y)
.text(d => d.icon);
.text(d => (d.icon.startsWith("http") || d.icon.startsWith("data:image") ? "" : d.icon));
g.append("image")
.attr("class", "regimentImage")
.attr("x", d => x(d) - h)
.attr("y", d => y(d))
.attr("height", h)
.attr("width", h)
.attr("href", d => (d.icon.startsWith("http") || d.icon.startsWith("data:image") ? d.icon : ""));
};
const drawRegiment = function (reg, stateId) {
@ -84,7 +93,11 @@ const drawRegiment = function (reg, stateId) {
.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(Military.getTotal(reg));
g.append("text")
.attr("x", reg.x)
.attr("y", reg.y)
.attr("text-rendering", "optimizeSpeed")
.text(Military.getTotal(reg));
g.append("rect")
.attr("fill", "currentColor")
.attr("x", x1 - h)
@ -93,9 +106,17 @@ const drawRegiment = function (reg, stateId) {
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("text-rendering", "optimizeSpeed")
.attr("x", x1 - size)
.attr("y", reg.y)
.text(reg.icon);
.text(reg.icon.startsWith("http") || reg.icon.startsWith("data:image") ? "" : reg.icon);
g.append("image")
.attr("class", "regimentImage")
.attr("x", x1 - h)
.attr("y", y1)
.attr("height", h)
.attr("width", h)
.attr("href", reg.icon.startsWith("http") || reg.icon.startsWith("data:image") ? reg.icon : "");
};
// move one regiment to another
@ -122,5 +143,13 @@ const moveRegiment = function (reg, x, y) {
el.select(".regimentIcon")
.transition(move)
.attr("x", x1(x) - size)
.attr("y", y);
.attr("y", y)
.attr("height", "6")
.attr("width", "6");
el.select(".regimentImage")
.transition(move)
.attr("x", x1(x) - h)
.attr("y", y1(y))
.attr("height", "6")
.attr("width", "6");
};

View file

@ -0,0 +1,124 @@
"use strict";
function drawReliefIcons() {
TIME && console.time("drawRelief");
terrain.selectAll("*").remove();
const cells = pack.cells;
const density = terrain.attr("density") || 0.4;
const size = 2 * (terrain.attr("size") || 1);
const mod = 0.2 * size; // size modifier
const relief = [];
for (const i of cells.i) {
const height = cells.h[i];
if (height < 20) continue; // no icons on water
if (cells.r[i]) continue; // no icons on rivers
const biome = cells.biome[i];
if (height < 50 && biomesData.iconsDensity[biome] === 0) continue; // no icons for this biome
const polygon = getPackPolygon(i);
const [minX, maxX] = d3.extent(polygon, p => p[0]);
const [minY, maxY] = d3.extent(polygon, p => p[1]);
if (height < 50) placeBiomeIcons(i, biome);
else placeReliefIcons(i);
function placeBiomeIcons() {
const iconsDensity = biomesData.iconsDensity[biome] / 100;
const radius = 2 / iconsDensity / density;
if (Math.random() > iconsDensity * 10) return;
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
let h = (4 + Math.random()) * size;
const icon = getBiomeIcon(i, biomesData.icons[biome]);
if (icon === "#relief-grass-1") h *= 1.2;
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
function placeReliefIcons(i) {
const radius = 2 / density;
const [icon, h] = getReliefIcon(i, height);
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
function getReliefIcon(i, h) {
const temp = grid.cells.temp[pack.cells.g[i]];
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
const size = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6);
return [getIcon(type), size];
}
}
// sort relief icons by y+size
relief.sort((a, b) => a.y + a.s - (b.y + b.s));
const reliefHTML = new Array(relief.length);
for (const r of relief) {
reliefHTML.push(`<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`);
}
terrain.html(reliefHTML.join(""));
TIME && console.timeEnd("drawRelief");
function getBiomeIcon(i, b) {
let type = b[Math.floor(Math.random() * b.length)];
const temp = grid.cells.temp[pack.cells.g[i]];
if (type === "conifer" && temp < 0) type = "coniferSnow";
return getIcon(type);
}
function getVariant(type) {
switch (type) {
case "mount":
return rand(2, 7);
case "mountSnow":
return rand(1, 6);
case "hill":
return rand(2, 5);
case "conifer":
return 2;
case "coniferSnow":
return 1;
case "swamp":
return rand(2, 3);
case "cactus":
return rand(1, 3);
case "deadTree":
return rand(1, 2);
default:
return 2;
}
}
function getOldIcon(type) {
switch (type) {
case "mountSnow":
return "mount";
case "vulcan":
return "mount";
case "coniferSnow":
return "conifer";
case "cactus":
return "dune";
case "deadTree":
return "dune";
default:
return type;
}
}
function getIcon(type) {
const set = terrain.attr("set") || "simple";
if (set === "simple") return "#relief-" + getOldIcon(type) + "-1";
if (set === "colored") return "#relief-" + type + "-" + getVariant(type);
if (set === "gray") return "#relief-" + type + "-" + getVariant(type) + "-bw";
return "#relief-" + getOldIcon(type) + "-1"; // simple
}
}

View file

@ -43,6 +43,7 @@ function drawScaleBar(scaleBar, scaleLevel) {
.data(d3.range(0, 6))
.enter()
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", d => rn((d * length) / 5, 2))
.attr("y", 0)
.attr("dy", "-.6em")
@ -52,6 +53,7 @@ function drawScaleBar(scaleBar, scaleLevel) {
if (label) {
texts
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", (length + 1) / 2)
.attr("dy", ".6em")
.attr("dominant-baseline", "text-before-edge")

View file

@ -106,6 +106,7 @@ function drawStateLabels(list) {
const textElement = textGroup
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", "stateLabel" + stateId)
.append("textPath")
.attr("startOffset", "50%")

View file

@ -131,7 +131,9 @@ class Battle {
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, " "));
headers += `<th data-tip="${label}">${u.icon}</th>`;
const isExternal = u.icon.startsWith("http") || u.icon.startsWith("data:image");
const iconHTML = isExternal ? `<img src="${u.icon}" width="15" height="15">` : u.icon;
headers += `<th data-tip="${label}">${iconHTML}</th>`;
}
headers += "<th data-tip='Total military''>Total</th></tr></thead>";
@ -145,9 +147,13 @@ class Battle {
const state = pack.states[regiment.state];
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScale) | 0; // distance between regiment and its base
const color = state.color[0] === "#" ? state.color : "#999";
const isExternal = regiment.icon.startsWith("http") || regiment.icon.startsWith("data:image");
const iconHtml = isExternal
? `<image href="${regiment.icon}" x="0.1em" y="0.1em" width="1.2em" height="1.2em"></image>`
: `<text x="50%" y="1em" style="text-anchor: middle">${regiment.icon}</text>`;
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em; stroke: #333">
<rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
<rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>${iconHtml}</svg>`;
const body = `<tbody id="battle${state.i}-${regiment.i}">`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${

View file

@ -317,7 +317,7 @@ function editBiomes() {
}
function regenerateIcons() {
ReliefIcons.draw();
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
}

View file

@ -275,6 +275,7 @@ function drawLegend(name, data) {
labels
.append("text")
.attr("text-rendering", "optimizeSpeed")
.text(data[i][2])
.attr("x", offset + colorBoxSize * 1.6)
.attr("y", fontSize / 1.6 + lineHeight + l * lineHeight + vOffset);
@ -285,6 +286,7 @@ function drawLegend(name, data) {
const offset = colOffset + legend.node().getBBox().width / 2;
labels
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "1.2em")
@ -976,25 +978,66 @@ function selectIcon(initial, callback) {
const cell = row.insertCell(i % 17);
cell.innerHTML = icons[i];
}
// find external images used as icons and show them
const externalResources = new Set();
const isExternal = url => url.startsWith("http") || url.startsWith("data:image");
options.military.forEach(unit => {
if (isExternal(unit.icon)) externalResources.add(unit.icon);
});
pack.states.forEach(state => {
state?.military?.forEach(regiment => {
if (isExternal(regiment.icon)) externalResources.add(regiment.icon);
});
});
externalResources.forEach(addExternalImage);
}
input.oninput = e => callback(input.value);
input.oninput = () => callback(input.value);
table.onclick = e => {
if (e.target.tagName === "TD") {
input.value = e.target.textContent;
callback(input.value);
}
};
table.onmouseover = e => {
if (e.target.tagName === "TD") tip(`Click to select ${e.target.textContent} icon`);
};
function addExternalImage(url) {
const addedIcons = byId("addedIcons");
const image = document.createElement("div");
image.style.cssText = `width: 2.2em; height: 2.2em; background-size: cover; background-image: url(${url})`;
addedIcons.appendChild(image);
image.onclick = () => callback(url);
}
byId("addImage").onclick = function () {
const input = this.previousElementSibling;
const ulr = input.value;
if (!ulr) return tip("Enter image URL to add", false, "error", 4000);
if (!ulr.match(/^((http|https):\/\/)|data\:image\//)) return tip("Enter valid URL", false, "error", 4000);
addExternalImage(ulr);
callback(ulr);
input.value = "";
};
byId("addedIcons")
.querySelectorAll("div")
.forEach(div => {
div.onclick = () => callback(div.style.backgroundImage.slice(5, -2));
});
$("#iconSelector").dialog({
width: fitContent(),
title: "Select Icon",
buttons: {
Apply: function () {
callback(input.value || "");
$(this).dialog("close");
},
Close: function () {
@ -1060,7 +1103,7 @@ function refreshAllEditors() {
// dynamically loaded editors
async function editStates() {
if (customization) return;
const Editor = await import("../dynamic/editors/states-editor.js?v=1.106.1");
const Editor = await import("../dynamic/editors/states-editor.js?v=1.108.1");
Editor.open();
}

View file

@ -192,7 +192,7 @@ function drawLayers() {
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleCompass")) compass.style("display", "block");
if (layerIsOn("toggleRivers")) drawRivers();
if (layerIsOn("toggleRelief")) ReliefIcons.draw();
if (layerIsOn("toggleRelief")) drawReliefIcons();
if (layerIsOn("toggleReligions")) drawReligions();
if (layerIsOn("toggleCultures")) drawCultures();
if (layerIsOn("toggleStates")) drawStates();
@ -424,13 +424,14 @@ function drawIce() {
const {temp, h} = cells;
Math.random = aleaPRNG(seed);
const ICEBERG_MAX_TEMP = 1;
const ICE_SHIELD_MAX_TEMP = -8;
const ICEBERG_MAX_TEMP = 0;
const GLACIER_MAX_TEMP = -8;
const minMaxTemp = d3.min(temp);
// very cold: draw ice shields
// cold land: draw glaciers
{
const type = "iceShield";
const getType = cellId => (temp[cellId] <= ICE_SHIELD_MAX_TEMP ? type : null);
const getType = cellId => (h[cellId] >= 20 && temp[cellId] <= GLACIER_MAX_TEMP ? type : null);
const isolines = getIsolines(grid, getType, {polygons: true});
isolines[type]?.polygons?.forEach(points => {
const clipped = clipPoly(points);
@ -438,21 +439,18 @@ function drawIce() {
});
}
// mildly cold: draw icebergs
// cold water: draw icebergs
for (const cellId of grid.cells.i) {
const t = temp[cellId];
if (t > ICEBERG_MAX_TEMP) continue; // too warm: no icebergs
if (t <= ICE_SHIELD_MAX_TEMP) continue; // already drawn as ice shield
if (h[cellId] >= 20) continue; // no icebergs on land
if (t > ICEBERG_MAX_TEMP) continue; // too warm: no icebergs
if (features[cells.f[cellId]].type === "lake") continue; // no icebers on lakes
if (P(0.8)) continue; // skip most of eligible cells
const tNormalized = normalize(t, -8, 2);
const randomFactor = t > -5 ? 0.4 + rand() * 1.2 : 1;
if (P(tNormalized ** 0.5 * randomFactor)) continue; // cold: skip some cells
let defaultSize = 1 - tNormalized; // iceberg size: 0 = zero size, 1 = full size
if (cells.t[cellId] === -1) defaultSize /= 1.3; // coasline: smaller icebergs
const size = minmax(rn(defaultSize * randomFactor, 2), 0.08, 1);
const randomFactor = 0.8 + rand() * 0.4; // random size factor
let baseSize = (1 - normalize(t, minMaxTemp, 1)) * 0.8; // size: 0 = zero size, 1 = full size
if (cells.t[cellId] === -1) baseSize /= 1.3; // coasline: smaller icebergs
const size = minmax(rn(baseSize * randomFactor, 2), 0.1, 1);
const [cx, cy] = grid.points[cellId];
const points = getGridPolygon(cellId).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]);
@ -723,6 +721,7 @@ function drawCoordinates() {
.data(data)
.enter()
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => d.text);
@ -743,7 +742,7 @@ function toggleCompass(event) {
function toggleRelief(event) {
if (!layerIsOn("toggleRelief")) {
turnButtonOn("toggleRelief");
if (!terrain.selectAll("*").size()) ReliefIcons.draw();
if (!terrain.selectAll("*").size()) drawReliefIcons();
$("#terrain").fadeIn();
if (event && isCtrlClick(event)) editStyle("terrain");
} else {

View file

@ -8,25 +8,24 @@ function editMarker(markerI) {
elSelected = d3.select(element).raise().call(d3.drag().on("start", dragMarker)).classed("draggable", true);
if (document.getElementById("notesEditor").offsetParent) editNotes(element.id, element.id);
if (byId("notesEditor").offsetParent) editNotes(element.id, element.id);
// dom elements
const markerType = document.getElementById("markerType");
const markerIcon = document.getElementById("markerIcon");
const markerIconSelect = document.getElementById("markerIconSelect");
const markerIconSize = document.getElementById("markerIconSize");
const markerIconShiftX = document.getElementById("markerIconShiftX");
const markerIconShiftY = document.getElementById("markerIconShiftY");
const markerSize = document.getElementById("markerSize");
const markerPin = document.getElementById("markerPin");
const markerFill = document.getElementById("markerFill");
const markerStroke = document.getElementById("markerStroke");
const markerType = byId("markerType");
const markerIconSelect = byId("markerIconSelect");
const markerIconSize = byId("markerIconSize");
const markerIconShiftX = byId("markerIconShiftX");
const markerIconShiftY = byId("markerIconShiftY");
const markerSize = byId("markerSize");
const markerPin = byId("markerPin");
const markerFill = byId("markerFill");
const markerStroke = byId("markerStroke");
const markerNotes = document.getElementById("markerNotes");
const markerLock = document.getElementById("markerLock");
const addMarker = document.getElementById("addMarker");
const markerAdd = document.getElementById("markerAdd");
const markerRemove = document.getElementById("markerRemove");
const markerNotes = byId("markerNotes");
const markerLock = byId("markerLock");
const addMarker = byId("addMarker");
const markerAdd = byId("markerAdd");
const markerRemove = byId("markerRemove");
updateInputs();
@ -39,8 +38,7 @@ function editMarker(markerI) {
const listeners = [
listen(markerType, "change", changeMarkerType),
listen(markerIcon, "input", changeMarkerIcon),
listen(markerIconSelect, "click", selectMarkerIcon),
listen(markerIconSelect, "click", changeMarkerIcon),
listen(markerIconSize, "input", changeIconSize),
listen(markerIconShiftX, "input", changeIconShiftX),
listen(markerIconShiftY, "input", changeIconShiftY),
@ -61,7 +59,7 @@ function editMarker(markerI) {
return [element, marker];
}
const element = document.getElementById(`marker${markerI}`);
const element = byId(`marker${markerI}`);
const marker = pack.markers.find(({i}) => i === markerI);
return [element, marker];
}
@ -97,19 +95,20 @@ function editMarker(markerI) {
}
function updateInputs() {
const {icon, type = "", size = 30, dx = 50, dy = 50, px = 12, stroke = "#000000", fill = "#ffffff", pin = "bubble", lock} = marker;
byId("markerIcon").innerHTML = marker.icon.startsWith("http") || marker.icon.startsWith("data:image")
? `<img src="${marker.icon}" style="width: 1em; height: 1em;">`
: marker.icon;
markerType.value = type;
markerIcon.value = icon;
markerIconSize.value = px;
markerIconShiftX.value = dx;
markerIconShiftY.value = dy;
markerSize.value = size;
markerPin.value = pin;
markerFill.value = fill;
markerStroke.value = stroke;
markerType.value = marker.type || "";
markerIconSize.value = marker.px || 12;
markerIconShiftX.value = marker.dx || 50;
markerIconShiftY.value = marker.dy || 50;
markerSize.value = marker.size || 30;
markerPin.value = marker.pin || "bubble";
markerFill.value = marker.fill || "#ffffff";
markerStroke.value = marker.stroke || "#000000";
markerLock.className = lock ? "icon-lock" : "icon-lock-open";
markerLock.className = marker.lock ? "icon-lock" : "icon-lock-open";
}
function changeMarkerType() {
@ -117,18 +116,12 @@ function editMarker(markerI) {
}
function changeMarkerIcon() {
const icon = this.value;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
});
}
selectIcon(marker.icon, value => {
const isExternal = value.startsWith("http") || value.startsWith("data:image");
byId("markerIcon").innerHTML = isExternal ? `<img src="${value}" style="width: 1em; height: 1em;">` : value;
function selectMarkerIcon() {
selectIcon(marker.icon, icon => {
markerIcon.value = icon;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
marker.icon = value;
redrawIcon(marker);
});
});
@ -165,7 +158,7 @@ function editMarker(markerI) {
getSameTypeMarkers().forEach(marker => {
marker.size = size;
const {i, x, y, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`);
const el = !hidden && byId(`marker${i}`);
if (!el) return;
const zoomedSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
@ -201,12 +194,23 @@ function editMarker(markerI) {
}
function redrawIcon({i, hidden, icon, dx = 50, dy = 50, px = 12}) {
const iconElement = !hidden && document.querySelector(`#marker${i} > text`);
if (iconElement) {
iconElement.innerHTML = icon;
iconElement.setAttribute("x", dx + "%");
iconElement.setAttribute("y", dy + "%");
iconElement.setAttribute("font-size", px + "px");
const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
const iconText = !hidden && document.querySelector(`#marker${i} > text`);
if (iconText) {
iconText.innerHTML = isExternal ? "" : icon;
iconText.setAttribute("x", dx + "%");
iconText.setAttribute("y", dy + "%");
iconText.setAttribute("font-size", px + "px");
}
const iconImage = !hidden && document.querySelector(`#marker${i} > image`);
if (iconImage) {
iconImage.setAttribute("x", dx / 2 + "%");
iconImage.setAttribute("y", dy / 2 + "%");
iconImage.setAttribute("width", px + "px");
iconImage.setAttribute("height", px + "px");
iconImage.setAttribute("href", isExternal ? icon : "");
}
}
@ -241,10 +245,10 @@ function editMarker(markerI) {
}
function deleteMarker() {
Markers.deleteMarker(marker.i)
Markers.deleteMarker(marker.i);
element.remove();
$("#markerEditor").dialog("close");
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
if (byId("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
}
function closeMarkerEditor() {

View file

@ -69,8 +69,14 @@ function overviewMarkers() {
function addLines() {
const lines = pack.markers
.map(({i, type, icon, pinned, lock}) => {
return `<div class="states" data-i=${i} data-type="${type}">
<div data-tip="Marker icon and type" style="width:12em">${icon} ${type}</div>
return /* html */ `
<div class="states" data-i=${i} data-type="${type}">
${
icon.startsWith("http") || icon.startsWith("data:image")
? `<img src="${icon}" data-tip="Marker icon" style="width:1.2em; height:1.2em; vertical-align: middle;">`
: `<span data-tip="Marker icon" style="width:1.2em">${icon}</span>`
}
<div data-tip="Marker type" style="width:10em">${type}</div>
<span style="padding-right:.1em" data-tip="Edit marker" class="icon-pencil"></span>
<span style="padding-right:.1em" data-tip="Focus on marker position" class="icon-dot-circled pointer"></span>
<span style="padding-right:.1em" data-tip="Pin marker (display only pinned markers)" class="icon-pin ${

View file

@ -284,7 +284,14 @@ function overviewMilitary() {
if (el.tagName !== "BUTTON") return;
const type = el.dataset.type;
if (type === "icon") return selectIcon(el.textContent, v => (el.textContent = v));
if (type === "icon") {
return selectIcon(el.textContent, function (value) {
el.innerHTML = value.startsWith("http") || value.startsWith("data:image")
? `<img src="${value}" style="width:1.2em;height:1.2em;pointer-events:none;">`
: value;
});
}
if (type === "biomes") {
const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null);
@ -329,9 +336,15 @@ function overviewMilitary() {
${getLimitText(unit[attr])}
</button>`;
row.innerHTML = /* html */ `<td><button data-type="icon" data-tip="Click to select unit icon">${
icon || " "
}</button></td>
row.innerHTML = /* html */ `<td>
<button data-type="icon" data-tip="Click to select unit icon">
${
icon.startsWith("http") || icon.startsWith("data:image")
? `<img src="${icon}" style="width:1.2em;height:1.2em;pointer-events:none;">`
: icon || ""
}
</button>
</td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${name}" /></td>
<td>${getLimitButton("biomes")}</td>
<td>${getLimitButton("states")}</td>
@ -427,7 +440,11 @@ function overviewMilitary() {
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] =
elements.map(el => {
const {type, value} = el.dataset || {};
if (type === "icon") return el.textContent || "";
if (type === "icon") {
const value = el.innerHTML.trim();
const isImage = value.startsWith("<img");
return isImage ? value.match(/src="([^"]*)"/)[1] : value || "";
}
if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
if (el.type === "number") return +el.value || 0;
if (el.type === "checkbox") return +el.checked || 0;

View file

@ -40,8 +40,8 @@ function editNotes(id, name) {
$("#notesEditor").dialog({
title: "Notes Editor",
width: window.innerWidth * 0.8,
height: window.innerHeight * 0.75,
width: svgWidth * 0.8,
height: svgHeight * 0.75,
position: {my: "center", at: "center", of: "svg"},
close: removeEditor
});

View file

@ -711,6 +711,7 @@ function editProvinces() {
node
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("dx", ".2em")
.attr("dy", "1em")
.attr("x", d => d.x0)

View file

@ -24,18 +24,17 @@ function editRegiment(selector) {
modules.editRegiment = true;
// add listeners
document.getElementById("regimentNameRestore").addEventListener("click", restoreName);
document.getElementById("regimentType").addEventListener("click", changeType);
document.getElementById("regimentName").addEventListener("change", changeName);
document.getElementById("regimentEmblem").addEventListener("input", changeEmblem);
document.getElementById("regimentEmblemSelect").addEventListener("click", selectEmblem);
document.getElementById("regimentAttack").addEventListener("click", toggleAttack);
document.getElementById("regimentRegenerateLegend").addEventListener("click", regenerateLegend);
document.getElementById("regimentLegend").addEventListener("click", editLegend);
document.getElementById("regimentSplit").addEventListener("click", splitRegiment);
document.getElementById("regimentAdd").addEventListener("click", toggleAdd);
document.getElementById("regimentAttach").addEventListener("click", toggleAttach);
document.getElementById("regimentRemove").addEventListener("click", removeRegiment);
byId("regimentNameRestore").addEventListener("click", restoreName);
byId("regimentType").addEventListener("click", changeType);
byId("regimentName").addEventListener("change", changeName);
byId("regimentEmblemChange").addEventListener("click", changeEmblem);
byId("regimentAttack").addEventListener("click", toggleAttack);
byId("regimentRegenerateLegend").addEventListener("click", regenerateLegend);
byId("regimentLegend").addEventListener("click", editLegend);
byId("regimentSplit").addEventListener("click", splitRegiment);
byId("regimentAdd").addEventListener("click", toggleAdd);
byId("regimentAttach").addEventListener("click", toggleAttach);
byId("regimentRemove").addEventListener("click", removeRegiment);
// get regiment data element
function getRegiment() {
@ -43,11 +42,13 @@ function editRegiment(selector) {
}
function updateRegimentData(regiment) {
document.getElementById("regimentType").className = regiment.n ? "icon-anchor" : "icon-users";
document.getElementById("regimentName").value = regiment.name;
document.getElementById("regimentEmblem").value = regiment.icon;
const composition = document.getElementById("regimentComposition");
byId("regimentType").className = regiment.n ? "icon-anchor" : "icon-users";
byId("regimentName").value = regiment.name;
byId("regimentEmblem").innerHTML = regiment.icon.startsWith("http") || regiment.icon.startsWith("data:image")
? `<img src="${regiment.icon}" style="width: 1em; height: 1em;">`
: regiment.icon;
const composition = byId("regimentComposition");
composition.innerHTML = options.military
.map(u => {
return `<div data-tip="${capitalize(u.name)} number. Input to change">
@ -126,12 +127,13 @@ function editRegiment(selector) {
function changeType() {
const reg = getRegiment();
reg.n = +!reg.n;
document.getElementById("regimentType").className = reg.n ? "icon-anchor" : "icon-users";
byId("regimentType").className = reg.n ? "icon-anchor" : "icon-users";
const size = +armies.attr("box-size");
const baseRect = elSelected.querySelectorAll("rect")[0];
const iconRect = elSelected.querySelectorAll("rect")[1];
const icon = elSelected.querySelector(".regimentIcon");
const image = elSelected.querySelector(".regimentIcon");
const x = reg.n ? reg.x - size * 2 : reg.x - size * 3;
baseRect.setAttribute("x", x);
baseRect.setAttribute("width", reg.n ? size * 4 : size * 6);
@ -148,19 +150,19 @@ function editRegiment(selector) {
const reg = getRegiment(),
regs = pack.states[elSelected.dataset.state].military;
const name = Military.getName(reg, regs);
elSelected.dataset.name = reg.name = document.getElementById("regimentName").value = name;
}
function selectEmblem() {
selectIcon(regimentEmblem.value, v => {
regimentEmblem.value = v;
changeEmblem();
});
elSelected.dataset.name = reg.name = byId("regimentName").value = name;
}
function changeEmblem() {
const emblem = document.getElementById("regimentEmblem").value;
getRegiment().icon = elSelected.querySelector(".regimentIcon").innerHTML = emblem;
const regiment = getRegiment();
selectIcon(regiment.icon, value => {
regiment.icon = value;
const isExternal = value.startsWith("http") || value.startsWith("data:image");
byId("regimentEmblem").innerHTML = isExternal ? `<img src="${value}" style="width: 1em; height: 1em;">` : value;
elSelected.querySelector(".regimentIcon").innerHTML = isExternal ? "" : value;
elSelected.querySelector(".regimentImage").setAttribute("href", isExternal ? value : "");
});
}
function changeUnit() {
@ -224,8 +226,8 @@ function editRegiment(selector) {
}
function toggleAdd() {
document.getElementById("regimentAdd").classList.toggle("pressed");
if (document.getElementById("regimentAdd").classList.contains("pressed")) {
byId("regimentAdd").classList.toggle("pressed");
if (byId("regimentAdd").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", addRegimentOnClick);
tip("Click on map to create new regiment or fleet", true);
} else {
@ -252,8 +254,8 @@ function editRegiment(selector) {
}
function toggleAttack() {
document.getElementById("regimentAttack").classList.toggle("pressed");
if (document.getElementById("regimentAttack").classList.contains("pressed")) {
byId("regimentAttack").classList.toggle("pressed");
if (byId("regimentAttack").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", attackRegimentOnClick);
tip("Click on another regiment to initiate battle", true);
armies.selectAll(":scope > g").classed("draggable", false);
@ -307,6 +309,7 @@ function editRegiment(selector) {
.on("end", () => new Battle(attacker, defender));
svg
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", window.innerWidth / 2)
.attr("y", window.innerHeight / 2)
.text("⚔️")
@ -324,8 +327,8 @@ function editRegiment(selector) {
}
function toggleAttach() {
document.getElementById("regimentAttach").classList.toggle("pressed");
if (document.getElementById("regimentAttach").classList.contains("pressed")) {
byId("regimentAttach").classList.toggle("pressed");
if (byId("regimentAttach").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", attachRegimentOnClick);
tip("Click on another regiment to unite both regiments. The current regiment will be removed", true);
armies.selectAll(":scope > g").classed("draggable", false);
@ -427,6 +430,7 @@ function editRegiment(selector) {
const text = this.querySelector("text");
const iconRect = this.querySelectorAll("rect")[1];
const icon = this.querySelector(".regimentIcon");
const image = this.querySelector(".regimentImage");
const self = elSelected === this;
const baseLine = viewbox.select("g#regimentBase > line");
@ -448,6 +452,8 @@ function editRegiment(selector) {
iconRect.setAttribute("y", y1);
icon.setAttribute("x", x1 - size);
icon.setAttribute("y", y);
image.setAttribute("x", x1 - h);
image.setAttribute("y", y1);
if (self) {
baseLine.attr("x2", x).attr("y2", y);
rotationControl
@ -479,9 +485,9 @@ function editRegiment(selector) {
viewbox.selectAll("g#regimentBase").remove();
armies.selectAll(":scope > g").classed("draggable", false);
armies.selectAll("g>g").call(d3.drag().on("drag", null));
document.getElementById("regimentAdd").classList.remove("pressed");
document.getElementById("regimentAttack").classList.remove("pressed");
document.getElementById("regimentAttach").classList.remove("pressed");
byId("regimentAdd").classList.remove("pressed");
byId("regimentAttack").classList.remove("pressed");
byId("regimentAttach").classList.remove("pressed");
restoreDefaultEvents();
elSelected = null;
}

View file

@ -67,14 +67,24 @@ function overviewRegiments(state) {
)
.join(" ");
lines += /* html */ `<div class="states" data-id="${r.i}" data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
lines += /* html */ `<div class="states" data-id="${r.i}" data-s="${s.i}" data-state="${s.name}" data-name="${
r.name
}" ${sortData} data-total="${r.a}">
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly />
<span data-tip="Regiment's emblem" style="width:1em">${r.icon}</span>
${
r.icon.startsWith("http") || r.icon.startsWith("data:image")
? `<img src="${r.icon}" data-tip="Regiment's emblem" style="width:1.2em; height:1.2em; vertical-align: middle;">`
: `<span data-tip="Regiment's emblem" style="width:1em">${r.icon}</span>`
}
<input data-tip="Regiment's name" style="width:13em" value="${r.name}" readonly />
${lineData}
<div data-type="total" data-tip="Total military personnel (not considering crew)" style="font-weight: bold">${r.a}</div>
<span data-tip="Edit regiment" onclick="editRegiment('#regiment${s.i}-${r.i}')" class="icon-pencil pointer"></span>
<div data-type="total" data-tip="Total military personnel (not considering crew)" style="font-weight: bold">${
r.a
}</div>
<span data-tip="Edit regiment" onclick="editRegiment('#regiment${s.i}-${
r.i
}')" class="icon-pencil pointer"></span>
</div>`;
regiments.push(r);

View file

@ -727,19 +727,19 @@ styleHeightmapCurve.on("change", e => {
styleReliefSet.on("change", e => {
terrain.attr("set", e.target.value);
ReliefIcons.draw();
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleReliefSize.on("change", e => {
terrain.attr("size", e.target.value);
ReliefIcons.draw();
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleReliefDensity.on("change", e => {
terrain.attr("density", e.target.value);
ReliefIcons.draw();
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});

View file

@ -79,7 +79,7 @@ function processFeatureRegeneration(event, button) {
$("#labels").fadeIn();
drawStateLabels();
} else if (button === "regenerateReliefIcons") {
ReliefIcons.draw();
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
} else if (button === "regenerateRoutes") {
regenerateRoutes();
@ -635,8 +635,10 @@ function addLabelOnClick() {
group.classed("hidden", false);
group
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", id)
.append("textPath")
.attr("text-rendering", "optimizeSpeed")
.attr("xlink:href", "#textPath_" + id)
.attr("startOffset", "50%")
.attr("font-size", "100%")
@ -877,33 +879,47 @@ function configMarkersGeneration() {
drawConfigTable();
function drawConfigTable() {
const {markers} = pack;
const config = Markers.getConfig();
const headers = `<thead style='font-weight:bold'><tr>
const headers = /* html */ `<thead style='font-weight:bold'><tr>
<td data-tip="Marker type name">Type</td>
<td data-tip="Marker icon">Icon</td>
<td data-tip="Marker number multiplier">Multiplier</td>
<td data-tip="Number of markers of that type on the current map">Number</td>
</tr></thead>`;
const lines = config.map(({type, icon, multiplier}, index) => {
const inputId = `markerIconInput${index}`;
return `<tr>
<td><input value="${type}" /></td>
const lines = config.map(({type, icon, multiplier}) => {
const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
return /* html */ `<tr>
<td><input class="type" value="${type}" /></td>
<td style="position: relative">
<input id="${inputId}" style="width: 5em" value="${icon}" />
<i class="icon-edit pointer" style="position: absolute; margin:.4em 0 0 -1.4em; font-size:.85em"></i>
<img class="image" src="${isExternal ? icon : ""}" ${
isExternal ? "" : "hidden"
} style="width:1.2em; height:1.2em; vertical-align: middle;">
<span class="emoji" style="font-size:1.2em">${isExternal ? "" : icon}</span>
<button class="changeIcon icon-pencil"></button>
</td>
<td><input type="number" min="0" max="100" step="0.1" value="${multiplier}" /></td>
<td style="text-align:center">${markers.filter(marker => marker.type === type).length}</td>
<td><input class="multiplier" type="number" min="0" max="100" step="0.1" value="${multiplier}" /></td>
<td style="text-align:center">${pack.markers.filter(marker => marker.type === type).length}</td>
</tr>`;
});
const table = `<table class="table">${headers}<tbody>${lines.join("")}</tbody></table>`;
alertMessage.innerHTML = table;
alertMessage.querySelectorAll("i").forEach(selectIconButton => {
alertMessage.querySelectorAll("button.changeIcon").forEach(selectIconButton => {
selectIconButton.addEventListener("click", function () {
const input = this.previousElementSibling;
selectIcon(input.value, icon => (input.value = icon));
const image = this.parentElement.querySelector(".image");
const emoji = this.parentElement.querySelector(".emoji");
const icon = image.getAttribute("src") || emoji.textContent;
selectIcon(icon, value => {
const isExternal = value.startsWith("http") || value.startsWith("data:image");
image.setAttribute("src", isExternal ? value : "");
image.hidden = !isExternal;
emoji.textContent = isExternal ? "" : value;
});
});
});
}
@ -911,12 +927,14 @@ function configMarkersGeneration() {
const applyChanges = () => {
const rows = alertMessage.querySelectorAll("tbody > tr");
const rowsData = Array.from(rows).map(row => {
const inputs = row.querySelectorAll("input");
return {
type: inputs[0].value,
icon: inputs[1].value,
multiplier: parseFloat(inputs[2].value)
};
const type = row.querySelector(".type").value;
const image = row.querySelector(".image");
const emoji = row.querySelector(".emoji");
const icon = image.getAttribute("src") || emoji.textContent;
const multiplier = parseFloat(row.querySelector(".multiplier").value);
return {type, icon, multiplier};
});
const config = Markers.getConfig();

View file

@ -248,10 +248,10 @@
},
"#ice": {
"opacity": 0.9,
"fill": "#e8f0f6",
"fill": "#f1f8fe",
"stroke": "#e8f0f6",
"stroke-width": 1,
"filter": "url(#dropShadow05)"
"stroke-width": 0.5,
"filter": "url(#dropShadow01)"
},
"#emblems": {
"opacity": 0.9,

View file

@ -4,6 +4,11 @@
// clip polygon by graph bbox
function clipPoly(points, secure = 0) {
if (points.length < 2) return points;
if (points.some(point => point === undefined)) {
ERROR && console.error("Undefined point in clipPoly", points);
return points;
}
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
}

View file

@ -12,7 +12,8 @@
*
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
*/
const VERSION = "1.107.0";
const VERSION = "1.109.0";
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
{
@ -36,6 +37,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
<ul>
<strong>Latest changes:</strong>
<li>Ability to set custom image as Marker or Regiment icon</li>
<li>Submap and Transform tools rework</li>
<li>Azgaar Bot to answer questions and provide help</li>
<li>Labels: ability to set letter spacing</li>