diff --git a/public/index.css b/public/index.css index d2061efd9a..85f9087f61 100644 --- a/public/index.css +++ b/public/index.css @@ -122,10 +122,9 @@ a { user-select: none; } -#population, -#cells, -#compass { - fill: none; +#cells, +#compass { + fill: none; } #landmass { @@ -175,12 +174,13 @@ a { } t, -#regions, -#cults, -#relig, -#biomes, -#provincesBody, -#terrs, +#regions, +#cults, +#relig, +#biomes, +#population, +#provincesBody, +#terrs, #tooltip, #temperature, #texture, @@ -213,21 +213,28 @@ t, stroke-linejoin: round; } -#statesBody, -#provincesBody, -#relig, -#biomes, -#cults { - stroke-linejoin: round; - fill-rule: evenodd; -} - -#statesBody, -#provincesBody, -#relig, -#cults { - mask: url(#land); -} +#statesBody, +#provincesBody, +#relig, +#biomes, +#population, +#cults { + stroke-linejoin: round; + fill-rule: evenodd; +} + +#population, +#population path { + stroke-dasharray: none !important; +} + +#statesBody, +#provincesBody, +#relig, +#population, +#cults { + mask: url(#land); +} #borders { stroke-linejoin: round; diff --git a/public/main.js b/public/main.js index ff5a68ab5d..7a6ca3816c 100644 --- a/public/main.js +++ b/public/main.js @@ -45,6 +45,7 @@ let texture = viewbox.append("g").attr("id", "texture"); let terrs = viewbox.append("g").attr("id", "terrs"); let lakes = viewbox.append("g").attr("id", "lakes"); let biomes = viewbox.append("g").attr("id", "biomes"); +let population = viewbox.append("g").attr("id", "population"); let cells = viewbox.append("g").attr("id", "cells"); let gridOverlay = viewbox.append("g").attr("id", "gridOverlay"); let coordinates = viewbox.append("g").attr("id", "coordinates"); @@ -72,7 +73,6 @@ let goods = viewbox.append("g").attr("id", "goods").style("display", "none"); let markets = viewbox.append("g").attr("id", "markets"); let tradeAnimation = viewbox.append("g").attr("id", "tradeAnimation"); let prec = viewbox.append("g").attr("id", "prec").style("display", "none"); -let population = viewbox.append("g").attr("id", "population"); let emblems = viewbox.append("g").attr("id", "emblems").style("display", "none"); let icons = viewbox.append("g").attr("id", "icons"); let labels = viewbox.append("g").attr("id", "labels"); diff --git a/public/modules/io/load.js b/public/modules/io/load.js index 80c0697d8c..d7e9fa0179 100644 --- a/public/modules/io/load.js +++ b/public/modules/io/load.js @@ -464,7 +464,10 @@ async function parseLoadedData(data, mapVersion) { if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders"); if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes"); if (hasChildren(temperature)) turnOn("toggleTemperature"); - if (hasChild(population, "line")) turnOn("togglePopulation"); + if (hasChild(population, "path, line")) { + turnOn("togglePopulation"); + drawPopulation(); + } if (isVisible(ice)) turnOn("toggleIce"); if (hasChild(prec, "circle")) turnOn("togglePrecipitation"); if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems"); diff --git a/public/modules/ui/layers.js b/public/modules/ui/layers.js index e4071ea1ef..54747763f1 100644 --- a/public/modules/ui/layers.js +++ b/public/modules/ui/layers.js @@ -359,76 +359,110 @@ function drawPrecipitation() { } function togglePopulation(event) { - if (!population.selectAll("line").size()) { + if (!population.selectAll("path, line").size()) { turnButtonOn("togglePopulation"); drawPopulation(); if (event && isCtrlClick(event)) editStyle("population"); } else { if (event && isCtrlClick(event)) return editStyle("population"); turnButtonOff("togglePopulation"); - - const isD3data = population.select("line").datum(); - if (!isD3data) { - // just remove - population.selectAll("line").remove(); - } else { - // remove with animation - const hide = d3.transition().duration(1000).ease(d3.easeSinIn); - population - .select("#rural") - .selectAll("line") - .transition(hide) - .attr("y2", d => d[1]) - .remove(); - population - .select("#urban") - .selectAll("line") - .transition(hide) - .delay(1000) - .attr("y2", d => d[1]) - .remove(); - } + population.selectAll("*").remove(); } } function drawPopulation() { - population.selectAll("line").remove(); + TIME && console.time("drawPopulation"); + + biomes.node().after(population.node()); + + population + .attr("mask", "url(#land)") + .attr("stroke", null) + .attr("stroke-width", null) + .attr("stroke-dasharray", null) + .attr("stroke-linecap", null) + .style("stroke", null) + .style("stroke-width", null) + .style("stroke-dasharray", null); const { cells, burgs } = pack; - const show = d3.transition().duration(2000).ease(d3.easeSinIn); + const urban = new Float64Array(cells.i.length); + const total = new Float64Array(cells.i.length); + let maxPopulation = 0; + + for (const cellId of cells.i) { + if (cells.h[cellId] < 20) continue; + + const burg = burgs[cells.burg[cellId]]; + const ruralPopulation = cells.pop[cellId] * populationRate; + urban[cellId] = + burg && !burg.removed ? (burg.population || 0) * populationRate * urbanization : 0; + total[cellId] = ruralPopulation + urban[cellId]; + if (total[cellId] > maxPopulation) maxPopulation = total[cellId]; + } - const rural = Array.from( - cells.i.filter(i => cells.pop[i] > 0), - i => [...cells.p[i], cells.p[i][1] - cells.pop[i] / 5] - ); + const colors = new Array(cells.i.length); + for (const cellId of cells.i) { + if (cells.h[cellId] < 20) continue; + colors[cellId] = getPopulationColor(urban[cellId], total[cellId], maxPopulation); + } - population - .select("#rural") - .selectAll("line") - .data(rural) - .enter() - .append("line") - .attr("x1", d => d[0]) - .attr("y1", d => d[1]) - .attr("x2", d => d[0]) - .attr("y2", d => d[1]) - .transition(show) - .attr("y2", d => d[2]); + const isolines = getIsolines(pack, cellId => colors[cellId] || null, { fill: true, waterGap: true }); + const populationShapes = Object.entries(isolines).map(([color, { fill, waterGap }], index) => { + const closedFill = closeSvgPathRings(fill); + const coastalClipId = `population-coast-clip${index}`; + const outsideFillClip = /* html */ ``; + const coastalStroke = waterGap + ? /* html */ `` + : ""; + const fillPath = /* html */ ``; + return { outsideFillClip, coastalStroke, fillPath }; + }); - const urban = burgs.filter(b => b.i && !b.removed).map(b => [b.x, b.y, b.y - (b.population / 5) * urbanization]); - population - .select("#urban") - .selectAll("line") - .data(urban) - .enter() - .append("line") - .attr("x1", d => d[0]) - .attr("y1", d => d[1]) - .attr("x2", d => d[0]) - .attr("y2", d => d[1]) - .transition(show) - .delay(500) - .attr("y2", d => d[2]); + const coastClips = populationShapes.map(({ outsideFillClip }) => outsideFillClip).join(""); + const coastalStrokes = populationShapes.map(({ coastalStroke }) => coastalStroke).join(""); + const fillPaths = populationShapes.map(({ fillPath }) => fillPath).join(""); + ensureEl("population").innerHTML = `${coastClips}${coastalStrokes}${fillPaths}`; + + TIME && console.timeEnd("drawPopulation"); +} + +function closeSvgPathRings(pathData) { + if (!pathData) return ""; + + return pathData + .split(/(?=M)/) + .filter(Boolean) + .map(ring => (/[zZ]\s*$/.test(ring) ? ring : `${ring}Z`)) + .join(""); +} + +function getPopulationColor(urbanPopulation, totalPopulation, maxPopulation) { + if (!totalPopulation || !maxPopulation) return "#000000"; + + const urbanShare = urbanPopulation / totalPopulation; + const hue = 240 + 120 * urbanShare; + const value = (Math.log1p(totalPopulation) / Math.log1p(maxPopulation)) ** 1.5; + return hsvToHex(hue, 1, value); +} + +function hsvToHex(hueDegrees, saturation, value) { + const hue = ((hueDegrees % 360) + 360) % 360; + const chroma = value * saturation; + const segment = hue / 60; + const intermediate = chroma * (1 - Math.abs((segment % 2) - 1)); + + let [red, green, blue] = [0, 0, 0]; + if (segment < 1) [red, green] = [chroma, intermediate]; + else if (segment < 2) [red, green] = [intermediate, chroma]; + else if (segment < 3) [green, blue] = [chroma, intermediate]; + else if (segment < 4) [green, blue] = [intermediate, chroma]; + else if (segment < 5) [red, blue] = [intermediate, chroma]; + else [red, blue] = [chroma, intermediate]; + + const match = value - chroma; + const toHex = channel => Math.round((channel + match) * 255).toString(16).padStart(2, "0"); + return `#${toHex(red)}${toHex(green)}${toHex(blue)}`; } function toggleCells(event) { diff --git a/public/modules/ui/style-presets.js b/public/modules/ui/style-presets.js index b129c76b0a..92dc524096 100644 --- a/public/modules/ui/style-presets.js +++ b/public/modules/ui/style-presets.js @@ -221,7 +221,7 @@ function addStylePreset() { "#landmass": ["opacity", "fill", "filter"], "#markers": ["opacity", "rescale", "filter"], "#prec": ["opacity", "stroke", "stroke-width", "fill", "filter"], - "#population": ["opacity", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"], + "#population": ["opacity", "filter"], "#markets": ["opacity", "stroke-width", "fill-opacity", "stroke-opacity", "filter"], "#goodsCells": ["opacity", "filter"], "#goodsIcons": ["opacity", "stroke-width", "data-circle", "filter"], diff --git a/public/modules/ui/style.js b/public/modules/ui/style.js index 274a26647b..1937b72b37 100644 --- a/public/modules/ui/style.js +++ b/public/modules/ui/style.js @@ -146,7 +146,6 @@ function selectStyleElement() { "coordinates", "gridOverlay", "legend", - "population", "routes", "temperature", "zones" @@ -165,7 +164,6 @@ function selectStyleElement() { "compass", "coordinates", "gridOverlay", - "population", "prec", "routes", "temperature", @@ -227,18 +225,6 @@ function selectStyleElement() { styleReliefSet.value = terrain.attr("set"); } - if (styleElement === "population") { - stylePopulation.style.display = "block"; - stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population - .select("#rural") - .attr("stroke"); - stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population - .select("#urban") - .attr("stroke"); - styleStrokeWidth.style.display = "block"; - styleStrokeWidthInput.value = el.attr("stroke-width") || 0; - } - if (styleElement === "regions") { styleStates.style.display = "block"; styleStatesBodyOpacity.value = statesBody.attr("opacity") || 1; diff --git a/src/index.html b/src/index.html index 7873ad831f..a541fb7475 100644 --- a/src/index.html +++ b/src/index.html @@ -133,7 +133,7 @@ + Population +
  • Precipitation
  • -
  • - Population -
  • - + - + - - + +