state only borders + watercolor style

This commit is contained in:
Azgaar 2021-07-10 20:08:30 +03:00
parent 44b3911e65
commit 687dedfe1b
9 changed files with 279 additions and 191 deletions

View file

@ -171,11 +171,18 @@ a {
#statesBody, #statesBody,
#provincesBody { #provincesBody {
stroke-width: 2; stroke-width: 3;
stroke-linejoin: round;
fill-rule: evenodd; fill-rule: evenodd;
mask: url(#land); mask: url(#land);
} }
#statesHalo {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
#relig, #relig,
#biomes, #biomes,
#cults { #cults {
@ -183,11 +190,6 @@ a {
mask: url(#land); mask: url(#land);
} }
#statesHalo {
fill: none;
filter: url(#blur5);
}
#borders { #borders {
stroke-linejoin: round; stroke-linejoin: round;
fill: none; fill: none;

View file

@ -322,6 +322,7 @@
<option value="styleAncient" data-system=1>Ancient</option> <option value="styleAncient" data-system=1>Ancient</option>
<option value="styleGloom" data-system=1>Gloom</option> <option value="styleGloom" data-system=1>Gloom</option>
<option value="styleClean" data-system=1>Clean</option> <option value="styleClean" data-system=1>Clean</option>
<option value="styleWatercolor" data-system=1>Watercolor</option>
<option value="styleMonochrome" data-system=1>Monochrome (for heightmap)</option> <option value="styleMonochrome" data-system=1>Monochrome (for heightmap)</option>
</select> </select>
<button id="addStyleButton" data-tip="Click to save current style as a new preset" class="icon-plus styleButton" style="display: inline-block" onclick="addStylePreset()"></button> <button id="addStyleButton" data-tip="Click to save current style as a new preset" class="icon-plus styleButton" style="display: inline-block" onclick="addStylePreset()"></button>
@ -363,16 +364,15 @@
<option value="compass">Wind Rose</option> <option value="compass">Wind Rose</option>
<option value="zones">Zones</option> <option value="zones">Zones</option>
</select> </select>
<!-- <button id="restoreStyle" data-tip="Click to restore default style for all elements" class="icon-ccw styleButton" onclick="askToRestoreDefaultStyle()"></button> -->
<table id="styleElements"> <table id="styleElements">
<caption id="styleIsOff" data-tip="The selected layer is not visible. See the buttons above to toggle it on">Please ensure the element is toggled on!</caption> <caption id="styleIsOff" data-tip="The selected layer is not visible. Toogle it on to see style changes effect">Ensure the element visibility is toggled on!</caption>
<tbody id="styleGroup"> <tbody id="styleGroup">
<tr data-tip="Select element group"> <tr data-tip="Select element group">
<td><b>Group</b></td> <td><b>Group</b></td>
<td> <td>
<select id="styleGroupSelect"><option value="regions">regions</option></select> <select id="styleGroupSelect"></select>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -387,24 +387,6 @@
</tr> </tr>
</tbody> </tbody>
<tbody id="styleStates" style="display: block">
<tr data-tip="Set states halo effect width">
<td>Halo width</td>
<td>
<input id="styleStatesHaloWidth" type="range" min=0 max=30 step=.1 value=10>
<output id="styleStatesHaloWidthOutput">10</output>
</td>
</tr>
<tr data-tip="Set states halo effect opacity. 0: invisible, 1: solid">
<td>Halo opacity</td>
<td>
<input id="styleStatesHaloOpacity" type="range" min=0 max=1 step=0.01 value=1>
<output id="styleStatesHaloOpacityOutput">1</output>
</td>
</tr>
</tbody>
<tbody id="styleLegend"> <tbody id="styleLegend">
<tr data-tip="Set maximum number of items in one column"> <tr data-tip="Set maximum number of items in one column">
<td>Column items</td> <td>Column items</td>
@ -757,6 +739,40 @@
</tr> </tr>
</tbody> </tbody>
<tbody id="styleStates" style="display: block">
<tr data-tip="Set states fill opacity. 0: invisible, 1: solid">
<td>Body opacity</td>
<td>
<input id="styleStatesBodyOpacity" type="range" min=0 max=1 step=0.01>
<output id="styleStatesBodyOpacityOutput"></output>
</td>
</tr>
<tr data-tip="Set states halo effect width">
<td>Halo width</td>
<td>
<input id="styleStatesHaloWidth" type="range" min=0 max=30 step=.1 value=10>
<output id="styleStatesHaloWidthOutput">10</output>
</td>
</tr>
<tr data-tip="Set states halo effect opacity. 0: invisible, 1: solid">
<td>Halo opacity</td>
<td>
<input id="styleStatesHaloOpacity" type="range" min=0 max=1 step=0.01 value=1>
<output id="styleStatesHaloOpacityOutput">1</output>
</td>
</tr>
<tr data-tip="Select halo effect power (blur). Set to 0 to make it solid line">
<td>Halo blur</td>
<td>
<input id="styleStatesHaloBlur" type="range" min=0 max=10 step=0.01 value=4>
<output id="styleStatesHaloBlurOutput">4</output>
</td>
</tr>
</tbody>
<tbody id="styleHeightmap"> <tbody id="styleHeightmap">
<tr data-tip="Select color scheme for the element"> <tr data-tip="Select color scheme for the element">
<td>Color scheme</td> <td>Color scheme</td>

View file

@ -2,7 +2,7 @@
// https://github.com/Azgaar/Fantasy-Map-Generator // https://github.com/Azgaar/Fantasy-Map-Generator
"use strict"; "use strict";
const version = "1.63"; // generator version const version = "1.64"; // generator version
document.title += " v" + version; document.title += " v" + version;
// Switches to disable/enable logging features // Switches to disable/enable logging features
@ -506,13 +506,14 @@ function invokeActiveZooming() {
// change states halo width // change states halo width
if (!customization) { if (!customization) {
const haloSize = rn(statesHalo.attr("data-width") / scale, 1); const desired = +statesHalo.attr("data-width");
statesHalo.attr("stroke-width", haloSize).style("display", haloSize > 3 ? "block" : "none"); const haloSize = rn(desired / scale ** 0.8, 2);
statesHalo.attr("stroke-width", haloSize).style("display", haloSize > 0.1 ? "block" : "none");
} }
// rescale map markers // rescale map markers
if (+markers.attr("rescale") && markers.style("display") !== "none") { if (+markers.attr("rescale") && markers.style("display") !== "none") {
markers.selectAll("use").each(function (d) { markers.selectAll("use").each(function () {
const x = +this.dataset.x, const x = +this.dataset.x,
y = +this.dataset.y, y = +this.dataset.y,
desired = +this.dataset.size; desired = +this.dataset.size;

View file

@ -10,8 +10,8 @@
pack.features.forEach(f => { pack.features.forEach(f => {
if (f.type !== "lake") return; if (f.type !== "lake") return;
// default flux: sum of precipition around lake first cell // default flux: sum of precipitation around lake
f.flux = rn(d3.sum(f.shoreline.map(c => grid.cells.prec[cells.g[c]])) / 2); f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
// temperature and evaporation to detect closed lakes // temperature and evaporation to detect closed lakes
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1); f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
@ -96,7 +96,6 @@
if (feature.type !== "lake") continue; if (feature.type !== "lake") continue;
delete feature.river; delete feature.river;
delete feature.enteringFlux; delete feature.enteringFlux;
delete feature.shoreline;
delete feature.outCell; delete feature.outCell;
delete feature.closed; delete feature.closed;
feature.height = rn(feature.height, 3); feature.height = rn(feature.height, 3);
@ -140,7 +139,7 @@
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava"; if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (!feature.inlets && !feature.outlet) { if (!feature.inlets && !feature.outlet) {
if (feature.evaporation / 2 > feature.flux) return "dry"; if (feature.evaporation > feature.flux * 4) return "dry";
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole"; if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
} }

View file

@ -681,7 +681,7 @@ function parseLoadedData(data) {
} }
if (version < 1.63) { if (version < 1.63) {
// v.1.63 change ocean pattern opacity element // v.1.63 changed ocean pattern opacity element
const oceanPattern = document.getElementById("oceanPattern"); const oceanPattern = document.getElementById("oceanPattern");
if (oceanPattern) oceanPattern.removeAttribute("opacity"); if (oceanPattern) oceanPattern.removeAttribute("opacity");
const oceanicPattern = document.getElementById("oceanicPattern"); const oceanicPattern = document.getElementById("oceanicPattern");
@ -693,6 +693,14 @@ function parseLoadedData(data) {
labels.select("#states").style("text-shadow", "white 0 0 4px"); labels.select("#states").style("text-shadow", "white 0 0 4px");
labels.select("#addedLabels").style("text-shadow", "white 0 0 4px"); labels.select("#addedLabels").style("text-shadow", "white 0 0 4px");
} }
if (version < 1.64) {
// v.1.64 change states style
const bodyOpacity = regions.attr("opacity");
statesBody.attr("opacity", bodyOpacity);
statesHalo.attr("opacity", bodyOpacity).attr("filter", "blur(5px)");
regions.removeAttribute("opacity");
}
})(); })();
void (function checkDataIntegrity() { void (function checkDataIntegrity() {

View file

@ -600,10 +600,7 @@ function getRiverPoints(node) {
} }
async function quickSave() { async function quickSave() {
if (customization) { if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
return;
}
const blob = await getMapData(); const blob = await getMapData();
if (blob) ldb.set("lastMap", blob); // auto-save map if (blob) ldb.set("lastMap", blob); // auto-save map
tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000); tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000);

View file

@ -22,7 +22,7 @@ document.getElementById("exitCustomization").addEventListener("mousemove", showD
/** /**
* @param {string} tip Tooltip text * @param {string} tip Tooltip text
* @param {boolean} main Show above other tooltips * @param {boolean} main Show above other tooltips
* @param {string} type Message type (color): error, warn, success * @param {string} type Message type (color): error / warn / success
* @param {number} time Timeout to auto hide, ms * @param {number} time Timeout to auto hide, ms
*/ */
function tip(tip = "Tip is undefined", main, type, time) { function tip(tip = "Tip is undefined", main, type, time) {

View file

@ -880,30 +880,83 @@ function drawStates() {
TIME && console.time("drawStates"); TIME && console.time("drawStates");
regions.selectAll("path").remove(); regions.selectAll("path").remove();
const cells = pack.cells, const {cells, vertices, features} = pack;
vertices = pack.vertices, const states = pack.states;
states = pack.states, const n = cells.i.length;
n = cells.i.length;
const used = new Uint8Array(cells.i.length); const used = new Uint8Array(cells.i.length);
const vArray = new Array(states.length); // store vertices array const vArray = new Array(states.length); // store vertices array
const body = new Array(states.length).fill(""); // store path around each state const body = new Array(states.length).fill(""); // path around each state
const gap = new Array(states.length).fill(""); // store path along water for each state to fill the gaps const gap = new Array(states.length).fill(""); // path along water for each state to fill the gaps
const halo = new Array(states.length).fill(""); // path around states, but not lakes
// helper functions
const isLand = i => i[1] === "land";
const nextIsLand = (ar, i) => ar[i + 1]?.[1] === "land";
const prevIsLand = (ar, i) => ar[i - 1]?.[1] === "land";
const getStringPoint = v => vertices.p[v[0]].join(",");
// define inner-state lakes to omit on border render
const innerLakes = features.map(feature => {
if (feature.type !== "lake") return false;
if (!feature.shoreline) Lakes.getShoreline(feature);
const states = feature.shoreline.map(i => cells.state[i]);
return new Set(states).size > 1 ? false : true;
});
for (const i of cells.i) { for (const i of cells.i) {
if (!cells.state[i] || used[i]) continue; if (!cells.state[i] || used[i]) continue;
const s = cells.state[i]; const state = cells.state[i];
const onborder = cells.c[i].some(n => cells.state[n] !== s);
// if (state !== 5) continue;
const onborder = cells.c[i].some(n => cells.state[n] !== state);
if (!onborder) continue; if (!onborder) continue;
const borderWith = cells.c[i].map(c => cells.state[c]).find(n => n !== s); const borderWith = cells.c[i].map(c => cells.state[c]).find(n => n !== state);
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith)); const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
const chain = connectVertices(vertex, s, borderWith); const chain = connectVertices(vertex, state);
if (chain.length < 3) continue; if (chain.length < 3) continue;
const points = chain.map(v => vertices.p[v[0]]);
if (!vArray[s]) vArray[s] = []; // get path around state
vArray[s].push(points); const points = chain.filter(v => v[1] !== "innerLake").map(v => vertices.p[v[0]]);
body[s] += "M" + points.join("L"); if (!vArray[state]) vArray[state] = [];
gap[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r, v, i, d) => (!i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + "M" + vertices.p[v[0]] : r), "");
if (points.length) {
vArray[state].push(points);
body[state] += "M" + points.join("L");
}
// connect path for halo
let discontinued = true;
halo[state] += chain
.map((v, i) => {
if (isLand(v) || nextIsLand(chain, i) || prevIsLand(chain, i)) {
const operation = discontinued ? "M" : "L";
discontinued = false;
return `${operation}${getStringPoint(v)}`;
}
discontinued = true;
return "";
})
.join("");
// connect gaps between state and water into a single path
discontinued = true;
gap[state] += chain
.map(v => {
if (isLand(v)) {
discontinued = true;
return "";
}
const operation = discontinued ? "M" : "L";
discontinued = false;
return `${operation}${getStringPoint(v)}`;
})
.join("");
} }
// find state visual center // find state visual center
@ -912,54 +965,56 @@ function drawStates() {
states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
}); });
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]); const bodyData = body.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]); const gapData = gap.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const bodyString = bodyData.map(d => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join(""); const bodyString = bodyData.map(d => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join("");
const gapString = gapData.map(d => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join(""); const gapString = gapData.map(d => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join("");
const clipString = bodyData.map(d => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`).join(""); const clipString = bodyData.map(d => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`).join("");
const haloString = bodyData.map(d => `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666"}"/>`).join(""); const haloString = haloData.map(d => `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666"}"/>`).join("");
statesBody.html(bodyString + gapString); statesBody.html(bodyString + gapString);
defs.select("#statePaths").html(clipString); defs.select("#statePaths").html(clipString);
statesHalo.html(haloString); statesHalo.html(haloString);
// connect vertices to chain // connect vertices to chain
function connectVertices(start, t, state) { function connectVertices(start, state) {
const chain = []; // vertices chain to form a path const chain = []; // vertices chain to form a path
let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.state[c] !== t); const getType = c => {
function check(i) { const waterCell = c.find(i => cells.h[i] < 20);
state = cells.state[i]; if (!waterCell) return "land";
land = cells.h[i] >= 20; if (innerLakes[cells.f[waterCell]]) return "innerLake";
} return features[cells.f[waterCell]].type;
};
for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) { for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain const prev = chain.length ? chain[chain.length - 1][0] : -1; // previous vertex in chain
chain.push([current, state, land]); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.state[c] === t).forEach(c => (used[c] = 1)); chain.push([current, getType(c)]); // add current vertex to sequence
const c0 = c[0] >= n || cells.state[c[0]] !== t;
const c1 = c[1] >= n || cells.state[c[1]] !== t; c.filter(c => cells.state[c] === state).forEach(c => (used[c] = 1));
const c2 = c[2] >= n || cells.state[c[2]] !== t; const c0 = c[0] >= n || cells.state[c[0]] !== state;
const c1 = c[1] >= n || cells.state[c[1]] !== state;
const c2 = c[2] >= n || cells.state[c[2]] !== state;
const v = vertices.v[current]; // neighboring vertices const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) {
current = v[0]; if (v[0] !== prev && c0 !== c1) current = v[0];
check(c0 ? c[0] : c[1]); else if (v[1] !== prev && c1 !== c2) current = v[1];
} else if (v[1] !== prev && c1 !== c2) { else if (v[2] !== prev && c0 !== c2) current = v[2];
current = v[1];
check(c1 ? c[1] : c[2]); if (current === prev) {
} else if (v[2] !== prev && c0 !== c2) {
current = v[2];
check(c2 ? c[2] : c[0]);
}
if (current === chain[chain.length - 1][0]) {
ERROR && console.error("Next vertex is not found"); ERROR && console.error("Next vertex is not found");
break; break;
} }
} }
chain.push([start, state, land]); // add starting vertex to sequence to close the path
if (chain.length) chain.push(chain[0]);
return chain; return chain;
} }
invokeActiveZooming(); invokeActiveZooming();
TIME && console.timeEnd("drawStates"); TIME && console.timeEnd("drawStates");
} }

File diff suppressed because one or more lines are too long