diff --git a/index.html b/index.html index 7475ee752..72eedf3de 100644 --- a/index.html +++ b/index.html @@ -365,7 +365,6 @@ data-tip="Click to show the Menu" data-shortcut="Tab" class="options glow" - onclick="showOptions(event)" > ► @@ -391,7 +390,6 @@ data-tip="Click to hide the Menu" data-shortcut="Tab or Esc" class="options" - onclick="hideOptions(event)" > ◄ @@ -7657,8 +7655,8 @@ - + diff --git a/src/dialogs/dialogs/ice-editor.ts b/src/dialogs/dialogs/ice-editor.ts index f944560f7..210a188f7 100644 --- a/src/dialogs/dialogs/ice-editor.ts +++ b/src/dialogs/dialogs/ice-editor.ts @@ -21,7 +21,7 @@ export function open() { const type = elSelected.attr("type") ? "Glacier" : "Iceberg"; if (byId("iceRandomize")) byId("iceRandomize")!.style.display = type === "Glacier" ? "none" : "inline-block"; - const $iceSize = byId("iceSize") as HTMLInputElement; + const $iceSize = byId<'input'>("iceSize"); if ($iceSize) { $iceSize.style.display = type === "Glacier" ? "none" : "inline-block"; if (type === "Iceberg") $iceSize.value = elSelected.attr("size"); @@ -39,11 +39,11 @@ export function open() { isLoaded = true; // add listeners - byId("iceEditStyle")?.on("click", () => editStyle("ice")); - byId("iceRandomize")?.on("click", randomizeShape); - byId("iceSize")?.on("input", changeSize); - byId("iceNew")?.on("click", toggleAdd); - byId("iceRemove")?.on("click", removeIce); + byId("iceEditStyle").on("click", () => editStyle("ice")); + byId("iceRandomize").on("click", randomizeShape); + byId("iceSize").on("input", changeSize); + byId("iceNew").on("click", toggleAdd); + byId("iceRemove").on("click", removeIce); function randomizeShape() { const c = grid.points[+elSelected.attr("cell")]; diff --git a/src/layers/renderers/drawIce.js b/src/layers/renderers/drawIce.js deleted file mode 100644 index 3755b8cf1..000000000 --- a/src/layers/renderers/drawIce.js +++ /dev/null @@ -1,75 +0,0 @@ -import {getGridPolygon} from "utils/graphUtils"; -import {aleaPRNG} from "scripts/aleaPRNG"; -import {clipPoly} from "utils/lineUtils"; - -export function drawIce() { - const {cells, vertices} = grid; - const {temp, h} = cells; - const n = cells.i.length; - - const used = new Uint8Array(cells.i.length); - Math.random = aleaPRNG(seed); - - const shieldMin = -8; // max temp to form ice shield (glacier) - const icebergMax = 1; // max temp to form an iceberg - - for (const i of grid.cells.i) { - const t = temp[i]; - if (t > icebergMax) continue; // too warm: no ice - if (t > shieldMin && h[i] >= 20) continue; // non-glacier land: no ice - - if (t <= shieldMin) { - // very cold: ice shield - if (used[i]) continue; // already rendered - const onborder = cells.c[i].some(n => temp[n] > shieldMin); - if (!onborder) continue; // need to start from onborder cell - const vertex = cells.v[i].find(v => vertices.c[v].some(i => temp[i] > shieldMin)); - const chain = connectVertices(vertex); - if (chain.length < 3) continue; - const points = clipPoly(chain.map(v => vertices.p[v])); - ice.append("polygon").attr("points", points).attr("type", "iceShield"); - continue; - } - - // mildly cold: iceberd - if (P(normalize(t, -7, 2.5))) continue; // t[-5; 2] cold: skip some cells - if (grid.features[cells.f[i]].type === "lake") continue; // lake: no icebers - let size = (6.5 + t) / 10; // iceberg size: 0 = full size, 1 = zero size - if (cells.t[i] === -1) size *= 1.3; // coasline: smaller icebers - size = Math.min(size * (0.4 + Math.random() * 1.2), 0.95); // randomize iceberg size - resizePolygon(i, size); - } - - function resizePolygon(i, s) { - const c = grid.points[i]; - const points = getGridPolygon(i).map(p => [(p[0] + (c[0] - p[0]) * s) | 0, (p[1] + (c[1] - p[1]) * s) | 0]); - ice - .append("polygon") - .attr("points", points) - .attr("cell", i) - .attr("size", rn(1 - s, 2)); - } - - // connect vertices to chain - function connectVertices(start) { - const chain = []; // vertices chain to form a path - for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) { - const prev = last(chain); // previous vertex in chain - chain.push(current); // add current vertex to sequence - const c = vertices.c[current]; // cells adjacent to vertex - c.filter(c => temp[c] <= shieldMin).forEach(c => (used[c] = 1)); - const c0 = c[0] >= n || temp[c[0]] > shieldMin; - const c1 = c[1] >= n || temp[c[1]] > shieldMin; - const c2 = c[2] >= n || temp[c[2]] > shieldMin; - const v = vertices.v[current]; // neighboring vertices - if (v[0] !== prev && c0 !== c1) current = v[0]; - else if (v[1] !== prev && c1 !== c2) current = v[1]; - else if (v[2] !== prev && c0 !== c2) current = v[2]; - if (current === chain[chain.length - 1]) { - ERROR && console.error("Next vertex is not found"); - break; - } - } - return chain; - } -} diff --git a/src/layers/renderers/drawIce.ts b/src/layers/renderers/drawIce.ts new file mode 100644 index 000000000..c1251b7db --- /dev/null +++ b/src/layers/renderers/drawIce.ts @@ -0,0 +1,16 @@ +import { byId } from "utils/nodeUtils"; + +export function drawIce() { + const ice = byId("ice"); + const { ice: icePack } = pack; + + let innerHTML = ""; + for (const shield of icePack.iceShields) { + innerHTML += ``; + } + + for (const iceberg of icePack.icebergs) { + innerHTML += ``; + } + ice.innerHTML = innerHTML; +} diff --git a/src/modules/ui/options.ts b/src/modules/ui/options.ts index 5bd21b92b..21b915c3d 100644 --- a/src/modules/ui/options.ts +++ b/src/modules/ui/options.ts @@ -32,7 +32,8 @@ byId<'button'>("optionsTrigger").on("click", showOptions); byId<'button'>("optionsHide").on("click", hideOptions); // Window Objects -const {Zoom, COA, Cloud, ThreeD, Names} = window; +const {COA, Cloud, ThreeD, Names, Zoom} = window; + // DIV elements const tooltip = byId<'div'>("tooltip"); @@ -40,7 +41,8 @@ const tooltip = byId<'div'>("tooltip"); // Options pane elements const optionsTrigger = byId<'button'>("optionsTrigger"); const regenerate = byId<'button'>("regenerate"); -const optionsDiv = byId<'div'>("optionsContainer"); +const optionsContainer = byId<'div'>("optionsContainer"); +const optionsDisplay = byId<'div'>("options"); const collapsible = byId<'div'>("collapsible"); const layersContent = byId<'div'>("layersContent"); const styleContent = byId<'div'>("styleContent"); @@ -140,7 +142,7 @@ export function showOptions(event: Event) { } regenerate.style.display = "none"; - optionsDiv.style.display = "block"; + optionsDisplay.style.display = "block"; optionsTrigger.style.display = "none"; if (event) event.stopPropagation(); @@ -148,21 +150,21 @@ export function showOptions(event: Event) { // Hide options pane on trigger click export function hideOptions(event: Event) { - optionsDiv.style.display = "none"; + optionsDisplay.style.display = "none"; optionsTrigger.style.display = "block"; if (event) event.stopPropagation(); } // To toggle options on hotkey press export function toggleOptions(event: MouseEvent) { - if (optionsDiv.style.display === "none") showOptions(event); + if (optionsContainer.style.display === "none") showOptions(event); else hideOptions(event); } // Toggle "New Map!" pane on hover optionsTrigger.on("mouseenter", function () { if (optionsTrigger.classList.contains("glow")) return; - if (optionsDiv.style.display === "none") regenerate.style.display = "block"; + if (optionsContainer.style.display === "none") regenerate.style.display = "block"; }); collapsible.on("mouseleave", function () { @@ -170,17 +172,17 @@ collapsible.on("mouseleave", function () { }); // Activate options tab on click -optionsDiv +optionsContainer .querySelector("div.tab")! .on("click", function (event: any ) { // MARKER: any if (event.target.tagName !== "BUTTON") return; const id = event.target.id; - const active = optionsDiv.querySelector(".tab > button.active"); + const active = optionsContainer.querySelector(".tab > button.active"); if (active && id === active.id) return; // already active tab is clicked if (active) active.classList.remove("active"); - byId(id)!.classList.add("active"); - optionsDiv + byId(id).classList.add("active"); + optionsContainer .querySelectorAll(".tabcontent") .forEach((e: HTMLElement) => {e.style.display = "none"}); @@ -207,9 +209,9 @@ async function showSupporters() { } // on any option or dialog change -optionsDiv.on("change", storeValueIfRequired); +optionsContainer.on("change", storeValueIfRequired); dialogDiv.on("change", storeValueIfRequired); -optionsDiv.on("input", updateOutputToFollowInput); +optionsContainer.on("input", updateOutputToFollowInput); dialogDiv.on("input", updateOutputToFollowInput); function storeValueIfRequired(ev: any) { // MARKER: any @@ -320,10 +322,10 @@ function changeMapSize() { // just apply canvas size that was already set export function applyMapSize() { - const zoomMin = Number(zoomExtentMin.value); - const zoomMax = Number(zoomExtentMax.value); - graphWidth = Number(mapWidthInput.value); - graphHeight = Number(mapHeightInput.value); + const zoomMin = zoomExtentMin.valueAsNumber; + const zoomMax = zoomExtentMax.valueAsNumber; + graphWidth = mapWidthInput.valueAsNumber; + graphHeight = mapHeightInput.valueAsNumber; svgWidth = Math.min(graphWidth, window.innerWidth); svgHeight = Math.min(graphHeight, window.innerHeight); svg.attr("width", svgWidth).attr("height", svgHeight); @@ -537,7 +539,7 @@ function changeUIsize(value: number) { uiSizeInput.valueAsNumber = uiSizeOutput.valueAsNumber = value; document.getElementsByTagName("body")[0].style.fontSize = rn(value * 10, 2) + "px"; - optionsDiv.style.width = value * 300 + "px"; + optionsContainer.style.width = value * 300 + "px"; } function getUImaxSize() { @@ -1126,7 +1128,7 @@ async function enter3dView(type) { resizeStop: resize3d, close: enterStandardView }); - } else document.body.insertBefore(canvas, optionsDiv); + } else document.body.insertBefore(canvas, optionsContainer); toggle3dOptions(); } diff --git a/src/scripts/generation/pack/generateIce.ts b/src/scripts/generation/pack/generateIce.ts new file mode 100644 index 000000000..5947c16f8 --- /dev/null +++ b/src/scripts/generation/pack/generateIce.ts @@ -0,0 +1,101 @@ +import { ERROR } from "config/logging"; +import { aleaPRNG } from "scripts/aleaPRNG"; +import { clipPoly, getGridPolygonLocal, last, normalize, P, rn } from "utils"; + +export function generateIce( + grid: IGrid, + features: TGridFeatures, +): IIce { + const shieldMin = -8; // max temp to form ice shield (glacier) + const icebergMax = 1; // max temp to form an iceberg + const { cells: gridCells, points: gridPoints, vertices } = grid; + const nOfCells = gridCells.i.length; + const used = new Uint8Array(gridCells.i.length); + + Math.random = aleaPRNG(seed); + const icePack: IIce = { icebergs: [], iceShields: [] }; + for (const i of gridCells.i) { + const temperature = gridCells.temp[i]; + if (temperature > icebergMax) continue; // too warm: no ice + if (temperature > shieldMin && gridCells.h[i] >= 20) continue; // non-glacier land: no ice + + if (temperature <= shieldMin) { + // very cold: ice shield + if (used[i]) continue; // already rendered + const onborder = gridCells.c[i].some((n) => gridCells.temp[n] > shieldMin); + if (!onborder) continue; // need to start from onborder cell + const vertex = gridCells.v[i].find((v) => + vertices.c[v]?.some((i) => gridCells.temp[i] > shieldMin) + ); + if (vertex === undefined) continue; // no suitable vertex found + const chain = connectVertices(vertex); + if (chain.length < 3) continue; + const points = clipPoly(chain.map((v) => vertices.p[v])); + icePack.iceShields.push({ points, transform: { x: 0, y: 0 } }); + continue; + } + // mildly cold: iceberd + if (P(normalize(temperature, -7, 2.5))) continue; // t[-5; 2] cold: skip some cells + if ( + gridCells.f[i] !== 0 && + (features[gridCells.f[i]] as IGridFeature).type === "lake" + ) + continue; // lake: no icebers // MARKER as IGridFeature + let size = (6.5 + temperature) / 10; // iceberg size: 0 = full size, 1 = zero size + if (gridCells.t[i] === -1) size *= 1.3; // coasline: smaller icebers + size = Math.min(size * (0.4 + Math.random() * 1.2), 0.95); // randomize iceberg size + icePack.icebergs.push(generateIceberg(i, size)); + } + return icePack; + + // Helper functions + function generateIceberg(i: number, size: number): IiceBerg { + const cellMidPoint = gridPoints[i]; + const points: TPoints = getGridPolygonLocal(i, grid) + .map((point) => [ + (point[0] + (cellMidPoint[0] - point[0]) * size) | 0, + (point[1] + (cellMidPoint[1] - point[1]) * size) | 0, + ]); + return { + points, + transform: { x: 0, y: 0 }, + cell: i, + size: rn(1 - size, 2), + }; + } + + // connect vertices to chain + function connectVertices(start: number) { + const chain = []; // vertices chain to form a path + for ( + let i = 0, current = start; + i === 0 || (current !== start && i < 20000); + i++ + ) { + const prev = last(chain); // previous vertex in chain + chain.push(current); // add current vertex to sequence + const currentVertex = vertices.c[current]; // cells adjacent to vertex + currentVertex + .filter((cellIndicie) => gridCells.temp[cellIndicie] <= shieldMin) + .forEach((cellIndice) => (used[cellIndice] = 1)); + const c0 = + currentVertex[0] >= nOfCells || gridCells.temp[currentVertex[0]] > shieldMin; + const c1 = + currentVertex[1] >= nOfCells || gridCells.temp[currentVertex[1]] > shieldMin; + const c2 = + currentVertex[2] >= nOfCells || gridCells.temp[currentVertex[2]] > shieldMin; + const vertexNeighbors = vertices.v[current]; // neighboring vertices + if (vertexNeighbors[0] !== prev && c0 !== c1) + current = vertexNeighbors[0]; + else if (vertexNeighbors[1] !== prev && c1 !== c2) + current = vertexNeighbors[1]; + else if (vertexNeighbors[2] !== prev && c0 !== c2) + current = vertexNeighbors[2]; + if (current === chain[chain.length - 1]) { + ERROR && console.error("Next vertex is not found"); + break; + } + } + return chain; + } +} diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index 34a11b23b..89bc6634f 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -1,29 +1,30 @@ -import {markupPackFeatures} from "scripts/generation/markup"; -import {rankCells} from "scripts/generation/pack/rankCells"; -import {pick} from "utils/functionUtils"; -import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates"; -import {expandCultures} from "./cultures/expandCultures"; -import {generateCultures} from "./cultures/generateCultures"; -import {generateLakeNames} from "./lakes/generateLakeNames"; -import {generateProvinces} from "./provinces/generateProvinces"; -import {generateReligions} from "./religions/generateReligions"; -import {repackGrid} from "./repackGrid"; -import {generateRivers} from "./rivers/generateRivers"; -import {specifyRivers} from "./rivers/specifyRivers"; -import {generateRoutes} from "./routes/generateRoutes"; - -const {Biomes} = window; +import { markupPackFeatures } from "scripts/generation/markup"; +import { rankCells } from "scripts/generation/pack/rankCells"; +import { pick } from "utils/functionUtils"; +import { generateBurgsAndStates } from "./burgsAndStates/generateBurgsAndStates"; +import { expandCultures } from "./cultures/expandCultures"; +import { generateCultures } from "./cultures/generateCultures"; +import { generateLakeNames } from "./lakes/generateLakeNames"; +import { generateProvinces } from "./provinces/generateProvinces"; +import { generateReligions } from "./religions/generateReligions"; +import { repackGrid } from "./repackGrid"; +import { generateRivers } from "./rivers/generateRivers"; +import { specifyRivers } from "./rivers/specifyRivers"; +import { generateRoutes } from "./routes/generateRoutes"; +import { generateIce } from "./generateIce"; + +const { Biomes } = window; export function createPack(grid: IGrid): IPack { - const {temp, prec} = grid.cells; - const {vertices, cells} = repackGrid(grid); + const { temp, prec } = grid.cells; + const { vertices, cells } = repackGrid(grid); const { features: rawFeatures, featureIds, distanceField, haven, - harbor + harbor, } = markupPackFeatures(grid, vertices, pick(cells, "v", "c", "b", "p", "h")); const { @@ -32,9 +33,14 @@ export function createPack(grid: IGrid): IPack { riverIds, conf, rivers: rawRivers, - mergedFeatures + mergedFeatures, } = generateRivers( - {...pick(cells, "i", "c", "b", "g", "h", "p"), f: featureIds, t: distanceField, haven}, + { + ...pick(cells, "i", "c", "b", "g", "h", "p"), + f: featureIds, + t: distanceField, + haven, + }, rawFeatures, prec, temp @@ -47,10 +53,10 @@ export function createPack(grid: IGrid): IPack { riverIds, heights, neighbors: cells.c, - gridReference: cells.g + gridReference: cells.g, }); - const {suitability, population} = rankCells(mergedFeatures, { + const { suitability, population } = rankCells(mergedFeatures, { t: distanceField, f: featureIds, fl: flux, @@ -60,7 +66,7 @@ export function createPack(grid: IGrid): IPack { area: cells.area, biome, haven, - harbor + harbor, }); const cultures = generateCultures( @@ -78,7 +84,7 @@ export function createPack(grid: IGrid): IPack { fl: flux, s: suitability, pop: population, - biome + biome, }, temp ); @@ -92,42 +98,43 @@ export function createPack(grid: IGrid): IPack { r: riverIds, fl: flux, biome, - pop: population + pop: population, }); - const {burgIds, stateIds, burgs, states, conflicts} = generateBurgsAndStates( - cultures, - mergedFeatures, - temp, - rawRivers, - vertices, - { - ...pick(cells, "v", "c", "p", "b", "i", "g", "area"), - h: heights, - f: featureIds, - t: distanceField, - haven, - harbor, - r: riverIds, - fl: flux, - biome, - s: suitability, - pop: population, - culture: cultureIds - } - ); - - const {cellRoutes, routes} = generateRoutes(burgs, temp, { + const { burgIds, stateIds, burgs, states, conflicts } = + generateBurgsAndStates( + cultures, + mergedFeatures, + temp, + rawRivers, + vertices, + { + ...pick(cells, "v", "c", "p", "b", "i", "g", "area"), + h: heights, + f: featureIds, + t: distanceField, + haven, + harbor, + r: riverIds, + fl: flux, + biome, + s: suitability, + pop: population, + culture: cultureIds, + } + ); + + const { cellRoutes, routes } = generateRoutes(burgs, temp, { c: cells.c, p: cells.p, g: cells.g, h: heights, t: distanceField, biome, - burg: burgIds + burg: burgIds, }); - const {religionIds, religions} = generateReligions({ + const { religionIds, religions } = generateReligions({ states, cultures, burgs, @@ -143,30 +150,38 @@ export function createPack(grid: IGrid): IPack { culture: cultureIds, burg: burgIds, state: stateIds, - route: cellRoutes - } + route: cellRoutes, + }, }); - const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, mergedFeatures, vertices, { - i: cells.i, - c: cells.c, - v: cells.v, - h: heights, - t: distanceField, - f: featureIds, - culture: cultureIds, - state: stateIds, - burg: burgIds - }); + const { provinceIds, provinces } = generateProvinces( + states, + burgs, + cultures, + mergedFeatures, + vertices, + { + i: cells.i, + c: cells.c, + v: cells.v, + h: heights, + t: distanceField, + f: featureIds, + culture: cultureIds, + state: stateIds, + burg: burgIds, + } + ); const rivers = specifyRivers(rawRivers, cultureIds, cultures); const features = generateLakeNames(mergedFeatures, cultureIds, cultures); + const ice = generateIce(grid, features); // Military.generate(); // Markers.generate(); // addZones(); // add to pack data - const events: IEvents = {conflicts}; + const events: IEvents = { conflicts }; const pack: IPack = { vertices, @@ -188,7 +203,7 @@ export function createPack(grid: IGrid): IPack { state: stateIds, route: cellRoutes, religion: religionIds, - province: provinceIds + province: provinceIds, }, features, rivers, @@ -198,7 +213,8 @@ export function createPack(grid: IGrid): IPack { routes, religions, provinces, - events + events, + ice, }; return pack; diff --git a/src/scripts/generation/pack/repackGrid.ts b/src/scripts/generation/pack/repackGrid.ts index 48a32b1e1..a4c3bae96 100644 --- a/src/scripts/generation/pack/repackGrid.ts +++ b/src/scripts/generation/pack/repackGrid.ts @@ -9,6 +9,7 @@ import {calculateVoronoi} from "../graph"; const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD; + // repack grid cells: discart deep water cells, add land cells along the coast export function repackGrid(grid: IGrid) { TIME && console.time("repackGrid"); diff --git a/src/types/pack/ice.d.ts b/src/types/pack/ice.d.ts new file mode 100644 index 000000000..d16e09a6b --- /dev/null +++ b/src/types/pack/ice.d.ts @@ -0,0 +1,18 @@ + +interface IIceBase { + points: TPoints; + transform: {x: number, y: number}; +} + +interface IiceBerg extends IIceBase { + cell: number; + size: number; +} + +interface IiceShield extends IIceBase { +} + +interface IIce{ + icebergs: IiceBerg[]; + iceShields: IiceShield[]; +} \ No newline at end of file diff --git a/src/types/pack/pack.d.ts b/src/types/pack/pack.d.ts index 824431c18..275cbb14c 100644 --- a/src/types/pack/pack.d.ts +++ b/src/types/pack/pack.d.ts @@ -9,6 +9,7 @@ interface IPack extends IGraph { religions: TReligions; routes: TRoutes; events: IEvents; + ice: IIce; } interface IPackCells { diff --git a/src/utils/graphUtils.ts b/src/utils/graphUtils.ts index 963fcb1bf..cc60921a1 100644 --- a/src/utils/graphUtils.ts +++ b/src/utils/graphUtils.ts @@ -56,6 +56,10 @@ export function getGridPolygon(i: number): TPoints { return grid.cells.v[i].map(v => grid.vertices.p[v]); } +export function getGridPolygonLocal(i: number, grid: IGrid): TPoints { // TODO: find a better name + return grid.cells.v[i].map(v => grid.vertices.p[v]); +} + export function isLand(cellId: number) { return pack.cells.h[cellId] >= MIN_LAND_HEIGHT; }