diff --git a/src/index.css b/src/index.css index 9653262e..b5180a25 100644 --- a/src/index.css +++ b/src/index.css @@ -137,7 +137,8 @@ a { stroke-linejoin: round; } -t, +/* TODO: turn on after debugging */ +/* t, #regions, #cults, #relig, @@ -150,7 +151,7 @@ t, #landmass, #fogging { pointer-events: none; -} +} */ #armies text { pointer-events: none; diff --git a/src/layers/renderers/drawCultures.ts b/src/layers/renderers/drawCultures.ts index 7090f417..aa9189ef 100644 --- a/src/layers/renderers/drawCultures.ts +++ b/src/layers/renderers/drawCultures.ts @@ -1,22 +1,44 @@ import * as d3 from "d3"; import {getPaths} from "./utilts"; +import {pick} from "utils/functionUtils"; export function drawCultures() { - /* uses */ const {cells, vertices, cultures} = pack; + d3.select("#cults").selectAll("g").remove(); - const getType = (cellId: number) => cells.culture[cellId]; - const paths = getPaths(cells.c, cells.v, vertices, getType); + /* uses */ const {cells, vertices, features, cultures} = pack; - const getColor = (i: number) => i && (cultures[i] as ICulture).color; + const paths = getPaths({ + getType: (cellId: number) => cells.culture[cellId], + cells: pick(cells, "c", "v", "b", "h", "f"), + vertices, + features + }); + + const getColor = (i: number) => (cultures[i] as ICulture).color; d3.select("#cults") + .append("g") + .attr("fill", "none") + .attr("stroke-width", 3) .selectAll("path") .remove() - .data(Object.entries(paths)) + .data(paths) .enter() .append("path") - .attr("d", ([, path]) => path) + .attr("d", ([, path]) => path.waterGap) + .attr("stroke", ([i]) => getColor(Number(i))) + .attr("id", ([i]) => "culture-gap" + i); + + d3.select("#cults") + .append("g") + .attr("stroke", "none") + .selectAll("path") + .remove() + .data(paths) + .enter() + .append("path") + .attr("d", ([, path]) => path.fill) .attr("fill", ([i]) => getColor(Number(i))) .attr("id", ([i]) => "culture" + i); } diff --git a/src/layers/renderers/drawReligions.js b/src/layers/renderers/drawReligions.js index a8df70ae..e81810b4 100644 --- a/src/layers/renderers/drawReligions.js +++ b/src/layers/renderers/drawReligions.js @@ -4,7 +4,6 @@ export function drawReligions() { const n = cells.i.length; const used = new Uint8Array(cells.i.length); - const vArray = new Array(religions.length); // store vertices array const body = new Array(religions.length).fill(""); // store path around each religion const gap = new Array(religions.length).fill(""); // store path along water for each religion to fill the gaps @@ -20,8 +19,7 @@ export function drawReligions() { const chain = connectVertices(vertex, r, borderWith); if (chain.length < 3) continue; const points = chain.map(v => vertices.p[v[0]]); - if (!vArray[r]) vArray[r] = []; - vArray[r].push(points); + body[r] += "M" + points.join("L") + "Z"; gap[r] += "M" + diff --git a/src/layers/renderers/drawRoutes.ts b/src/layers/renderers/drawRoutes.ts index 1518dd8d..f6362490 100644 --- a/src/layers/renderers/drawRoutes.ts +++ b/src/layers/renderers/drawRoutes.ts @@ -12,7 +12,7 @@ const lineGenTypeMap: {[key in IRoute["type"]]: d3.CurveFactory | d3.CurveFactor export function drawRoutes() { routes.selectAll("path").remove(); - const {cells, burgs} = pack; + /* uses */ const {cells, burgs} = pack; const lineGen = d3.line(); const SHARP_ANGLE = 135; diff --git a/src/layers/renderers/utilts.ts b/src/layers/renderers/utilts.ts index cf454606..7931004a 100644 --- a/src/layers/renderers/utilts.ts +++ b/src/layers/renderers/utilts.ts @@ -1,40 +1,99 @@ +import {MIN_LAND_HEIGHT} from "config/generation"; import {connectVertices} from "scripts/connectVertices"; +import {isLake} from "utils/typeUtils"; -export function getPaths( - cellNeighbors: number[][], - cellVertices: number[][], - vertices: IGraphVertices, - getType: (cellId: number) => number -) { - const paths: Dict = {}; +type TPath = {fill: string; waterGap: string; halo: string}; - function addPath(index: number, points: TPoints) { - if (!paths[index]) paths[index] = ""; - paths[index] += "M" + points.join("L") + "Z"; - } +export function getPaths({ + vertices, + getType, + features, + cells +}: { + vertices: IGraphVertices; + getType: (cellId: number) => number; + features: TPackFeatures; + cells: Pick; +}) { + const paths: Dict = {}; - const checkedCells = new Uint8Array(cellNeighbors.length); - for (let cellId = 0; cellId < cellNeighbors.length; cellId++) { - if (checkedCells[cellId]) continue; - if (!getType(cellId)) continue; + const checkedCells = new Uint8Array(cells.c.length); + const addToChecked = (cellId: number) => { checkedCells[cellId] = 1; + }; + const isChecked = (cellId: number) => checkedCells[cellId] === 1; + + for (let cellId = 0; cellId < cells.c.length; cellId++) { + if (isChecked(cellId) || getType(cellId) === 0) continue; + addToChecked(cellId); const type = getType(cellId); const ofSameType = (cellId: number) => getType(cellId) === type; + const ofDifferentType = (cellId: number) => getType(cellId) !== type; - const isOnborder = cellNeighbors[cellId].some(cellId => !ofSameType(cellId)); - if (!isOnborder) continue; + const onborderCell = cells.c[cellId].find(ofDifferentType); + if (onborderCell === undefined) continue; - const startingVertex = cellVertices[cellId].find(v => vertices.c[v].some(cellId => !ofSameType(cellId))); - if (startingVertex === undefined) throw new Error(`getPath: starting vertex for cell ${cellId} is not found`); + const feature = features[cells.f[onborderCell]]; + if (isInnerLake(feature, ofSameType)) continue; - const chain = connectVertices({vertices, startingVertex, ofSameType, checkedCellsMutable: checkedCells}); + const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType)); + if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`); - if (chain.length < 3) continue; - const points = chain.map(v => vertices.p[v]); + const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true}); + if (vertexChain.length < 3) continue; - addPath(type, points); + addPath(type, vertexChain); } - return paths; + return Object.entries(paths); + + function getVertexPoint(vertex: number) { + return vertices.p[vertex]; + } + + function getFillPath(vertexChain: number[]) { + const points: TPoints = vertexChain.map(getVertexPoint); + const firstPoint = points.shift(); + return `M${firstPoint} L${points.join(" ")} Z`; + } + + function getBorderPath(vertexChain: number[], discontinue: (vertex: number) => boolean) { + let discontinued = true; + const path = vertexChain.map(vertex => { + if (discontinue(vertex)) { + discontinued = true; + return ""; + } + + const operation = discontinued ? "M" : "L"; + discontinued = false; + return ` ${operation}${getVertexPoint(vertex)}`; + }); + + return path.join("").trim(); + } + + function isBorderVertex(vertex: number) { + const adjacentCells = vertices.c[vertex]; + return adjacentCells.some(i => cells.b[i]); + } + + function isLandVertex(vertex: number) { + const adjacentCells = vertices.c[vertex]; + return adjacentCells.every(i => cells.h[i] >= MIN_LAND_HEIGHT); + } + + function addPath(index: number, vertexChain: number[]) { + if (!paths[index]) paths[index] = {fill: "", waterGap: "", halo: ""}; + + paths[index].fill += getFillPath(vertexChain); + paths[index].halo += getBorderPath(vertexChain, isBorderVertex); + paths[index].waterGap += getBorderPath(vertexChain, isLandVertex); + } +} + +function isInnerLake(feature: 0 | TPackFeature, ofSameType: (cellId: number) => boolean) { + if (!isLake(feature)) return false; + return feature.shoreline.every(ofSameType); } diff --git a/src/scripts/connectVertices.ts b/src/scripts/connectVertices.ts index cec69aba..afb8052b 100644 --- a/src/scripts/connectVertices.ts +++ b/src/scripts/connectVertices.ts @@ -88,38 +88,32 @@ function findStartingVertex({ throw new Error(`Markup: firstCell of feature ${featureId} has no neighbors of other features or external vertices`); } -const CONNECT_VERTICES_MAX_ITERATIONS = 50000; +const MAX_ITERATIONS = 50000; // connect vertices around feature export function connectVertices({ vertices, startingVertex, ofSameType, - checkedCellsMutable + addToChecked, + closeRing }: { vertices: IGraphVertices; startingVertex: number; ofSameType: (cellId: number) => boolean; - checkedCellsMutable?: Uint8Array; + addToChecked?: (cellId: number) => void; + closeRing?: boolean; }) { const chain: number[] = []; // vertices chain to form a path - const addToChecked = (cellIds: number[]) => { - if (checkedCellsMutable) { - cellIds.forEach(cellId => { - checkedCellsMutable[cellId] = 1; - }); - } - }; - let next = startingVertex; - for (let i = 0; i === 0 || (next !== startingVertex && i < CONNECT_VERTICES_MAX_ITERATIONS); i++) { + for (let i = 0; i === 0 || (next !== startingVertex && i < MAX_ITERATIONS); i++) { const previous = chain.at(-1); const current = next; chain.push(current); const neibCells = vertices.c[current]; - addToChecked(neibCells); + if (addToChecked) neibCells.forEach(addToChecked); const [c1, c2, c3] = neibCells.map(ofSameType); const [v1, v2, v3] = vertices.v[current]; @@ -134,5 +128,6 @@ export function connectVertices({ } } + if (closeRing) chain.push(startingVertex); return chain; } diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index a201620a..6d776f82 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -70,8 +70,10 @@ async function generate(options?: IGenerationOptions) { // renderLayer("biomes"); renderLayer("burgs"); renderLayer("routes"); + renderLayer("cultures"); + //renderLayer("religions"); - drawPolygons(pack.cells.religion, pack.cells.v, pack.vertices.p, {fillOpacity: 0.8, excludeZeroes: true}); + // drawPolygons(pack.cells.religion, pack.cells.v, pack.vertices.p, {fillOpacity: 0.8, excludeZeroes: true}); WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`); // showStatistics(); diff --git a/src/types/pack/features.d.ts b/src/types/pack/features.d.ts index 2ae8f95a..13cd5b22 100644 --- a/src/types/pack/features.d.ts +++ b/src/types/pack/features.d.ts @@ -35,6 +35,6 @@ interface IPackFeatureLake extends IPackFeatureBase { type TPackFeature = IPackFeatureOcean | IPackFeatureIsland | IPackFeatureLake; -type FirstElement = 0; +type TNoFeature = 0; -type TPackFeatures = [FirstElement, ...TPackFeature[]]; +type TPackFeatures = [TNoFeature, ...TPackFeature[]]; diff --git a/src/utils/lineUtils.ts b/src/utils/lineUtils.ts index 0d837f12..9674a7ad 100644 --- a/src/utils/lineUtils.ts +++ b/src/utils/lineUtils.ts @@ -63,12 +63,12 @@ function getPointOffCanvasSide([x, y]: TPoint) { // remove intermediate out-of-canvas points from polyline export function filterOutOfCanvasPoints(points: TPoints) { - const pointsOutSide = points.map(getPointOffCanvasSide); + const pointsOutside = points.map(getPointOffCanvasSide); const SAFE_ZONE = 3; - const fragment = (i: number) => sliceFragment(pointsOutSide, i, SAFE_ZONE); + const fragment = (i: number) => sliceFragment(pointsOutside, i, SAFE_ZONE); const filterOutCanvasPoint = (i: number) => { - const pointSide = pointsOutSide[i]; + const pointSide = pointsOutside[i]; return !pointSide || fragment(i).some(side => !side || side !== pointSide); }; diff --git a/src/utils/typeUtils.ts b/src/utils/typeUtils.ts index 94ce34aa..8248144b 100644 --- a/src/utils/typeUtils.ts +++ b/src/utils/typeUtils.ts @@ -1,3 +1,8 @@ +export const isFeature = (feature: TNoFeature | TPackFeature): feature is TPackFeature => feature !== 0; + +export const isLake = (feature: TNoFeature | TPackFeature): feature is IPackFeatureLake => + isFeature(feature) && feature.type === "lake"; + export const isState = (state: TNeutrals | IState): state is IState => state.i !== 0 && !(state as IState).removed; export const isNeutals = (neutrals: TNeutrals | IState): neutrals is TNeutrals => neutrals.i === 0;