Fantasy-Map-Generator/modules/dynamic/heightmap-selection.js
2022-05-28 21:22:28 +03:00

343 lines
10 KiB
JavaScript

const templates = [
{id: "volcano", name: "Volcano"},
{id: "highIsland", name: "High Island"},
{id: "lowIsland", name: "Low Island"},
{id: "continents", name: "Continents"},
{id: "archipelago", name: "Archipelago"},
{id: "atoll", name: "Atoll"},
{id: "mediterranean", name: "Mediterranean"},
{id: "peninsula", name: "Peninsula"},
{id: "pangea", name: "Pangea"},
{id: "isthmus", name: "Isthmus"},
{id: "shattered", name: "Shattered"},
{id: "taklamakan", name: "Taklamakan"},
{id: "oldWorld", name: "Old World"},
{id: "fractious", name: "Fractious"}
];
const heightmaps = [
{id: "africa-centric", name: "Africa Centric"},
{id: "arabia", name: "Arabia"},
{id: "atlantics", name: "Atlantics"},
{id: "britain", name: "Britain"},
{id: "caribbean", name: "Caribbean"},
{id: "east-asia", name: "East Asia"},
{id: "eurasia", name: "Eurasia"},
{id: "europe", name: "Europe"},
{id: "europe-accented", name: "Europe Accented"},
{id: "europe-and-central-asia", name: "Europe and Central Asia"},
{id: "europe-central", name: "Europe Central"},
{id: "europe-north", name: "Europe North"},
{id: "greenland", name: "Greenland"},
{id: "hellenica", name: "Hellenica"},
{id: "iceland", name: "Iceland"},
{id: "indian-ocean", name: "Indian Ocean"},
{id: "mediterranean-sea", name: "Mediterranean Sea"},
{id: "middle-east", name: "Middle East"},
{id: "north-america", name: "North America"},
{id: "us-centric", name: "US-centric"},
{id: "us-mainland", name: "US Mainland"},
{id: "world", name: "World"},
{id: "world-from-pacific", name: "World from Pacific"}
];
const initialSeed = generateSeed();
appendStyleSheet();
insertEditorHtml();
addListeners();
export function open() {
closeDialogs(".stable");
const $templateInput = byId("templateInput");
setSelected($templateInput.value);
$("#heightmapSelection").dialog({
title: "Select Heightmap",
resizable: false,
position: {my: "center", at: "center", of: "svg"},
buttons: {
Cancel: function () {
$(this).dialog("close");
},
Select: function () {
const id = getSelected();
$templateInput.value = id;
lock("template");
$(this).dialog("close");
},
"New Map": function () {
const id = getSelected();
$templateInput.value = id;
lock("template");
const seed = getSeed();
Math.random = aleaPRNG(seed);
regeneratePrompt({seed});
$(this).dialog("close");
}
}
});
}
function appendStyleSheet() {
const styles = /* css */ `
div.dialog > div.heightmap-selection {
width: 70vw;
height: 70vh;
}
.heightmap-selection_container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
grid-gap: 6px;
}
@media (max-width: 600px) {
.heightmap-selection_container {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
grid-gap: 4px;
}
}
@media (min-width: 2000px) {
.heightmap-selection_container {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 8px;
}
}
.heightmap-selection_options {
display: grid;
grid-template-columns: 2fr 1fr;
justify-items: start;
}
.heightmap-selection_options > div:first-child {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
}
.heightmap-selection article {
padding: 4px;
border-radius: 8px;
transition: all 0.1s ease-in-out;
filter: drop-shadow(1px 1px 4px #999);
}
.heightmap-selection article:hover {
background-color: #ddd;
filter: drop-shadow(1px 1px 8px #999);
cursor: pointer;
}
.heightmap-selection article.selected {
background-color: #ccc;
outline: 1px solid var(--dark-solid);
filter: drop-shadow(1px 1px 8px #999);
}
.heightmap-selection article > div {
display: flex;
justify-content: space-between;
padding: 2px 1px;
}
.heightmap-selection article > div > span.icon-cw:hover {
color: #000;
}
.heightmap-selection article > div > span.icon-cw:active {
color: #666;
}
.heightmap-selection article > img {
width: 100%;
aspect-ratio: ${graphWidth}/${graphHeight};
border-radius: 8px;
object-fit: fill;
}
`;
const style = document.createElement("style");
style.appendChild(document.createTextNode(styles));
document.head.appendChild(style);
}
function insertEditorHtml() {
const heightmapSelectionHtml = /* html */ `<div id="heightmapSelection" class="dialog stable">
<div class="heightmap-selection">
<section>
<header><h1>Heightmap templates</h1></header>
<div class="heightmap-selection_container"></div>
</section>
<section>
<header><h1>Pre-created heightmaps</h1></header>
<div class="heightmap-selection_container"></div>
</section>
<section>
<header><h1>Options</h1></header>
<div class="heightmap-selection_options">
<div>
<div>
<input id="heightmapSelectionRenderOcean" class="checkbox" type="checkbox" />
<label for="heightmapSelectionRenderOcean" class="checkbox-label">Render ocean heights</label>
</div>
<div>
Color scheme
<select id="heightmapSelectionColorScheme">
<option value="bright" selected>Bright</option>
<option value="light">Light</option>
<option value="green">Green</option>
<option value="monochrome">Monochrome</option>
</select>
</div>
</div>
<div>
<button data-tip="Rerender all preview images" id="heightmapSelectionRedrawPreview">Redraw preview</button>
<button data-tip="Open Template Editor" data-tool="templateEditor" id="heightmapSelectionEditTemplates">Edit Templates</button>
<button data-tip="Open Image Converter" data-tool="imageConverter" id="heightmapSelectionImportHeightmap">Import Heightmap</button>
</div>
</div>
</section>
</div>
</div>`;
byId("dialogs").insertAdjacentHTML("beforeend", heightmapSelectionHtml);
const sections = document.getElementsByClassName("heightmap-selection_container");
sections[0].innerHTML = templates
.map(({id, name}) => {
Math.random = aleaPRNG(initialSeed);
const heights = generateHeightmap(id);
const dataUrl = drawHeights(heights);
return /* html */ `<article data-id="${id}" data-seed="${initialSeed}">
<img src="${dataUrl}" alt="${name}" />
<div>
${name}
<span data-tip="Regenerate preview" class="icon-cw"></span>
</div>
</article>`;
})
.join("");
sections[1].innerHTML = heightmaps
.map(({id, name}) => {
drawPrecreatedHeightmap(id);
return /* html */ `<article data-id="${id}" data-seed="${initialSeed}">
<img alt="${name}" />
<div>${name}</div>
</article>`;
})
.join("");
}
function addListeners() {
byId("heightmapSelection").on("click", event => {
const article = event.target.closest("#heightmapSelection article");
if (!article) return;
const id = article.dataset.id;
if (event.target.matches("span.icon-cw")) {
const seed = generateSeed();
article.dataset.seed = seed;
Math.random = aleaPRNG(seed);
drawTemplatePreview(id);
} else setSelected(id);
});
byId("heightmapSelectionRenderOcean").on("change", redrawAll);
byId("heightmapSelectionColorScheme").on("change", redrawAll);
byId("heightmapSelectionRedrawPreview").on("click", redrawAll);
byId("heightmapSelectionEditTemplates").on("click", confirmHeightmapEdit);
byId("heightmapSelectionImportHeightmap").on("click", confirmHeightmapEdit);
}
function getSelected() {
return byId("heightmapSelection").querySelector(".selected")?.dataset?.id;
}
function setSelected(id) {
const $heightmapSelection = byId("heightmapSelection");
$heightmapSelection.querySelector(".selected")?.classList?.remove("selected");
$heightmapSelection.querySelector(`[data-id="${id}"]`)?.classList?.add("selected");
}
function getSeed() {
return byId("heightmapSelection").querySelector(".selected")?.dataset?.seed;
}
function drawHeights(heights) {
const canvas = document.createElement("canvas");
canvas.width = grid.cellsX;
canvas.height = grid.cellsY;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(grid.cellsX, grid.cellsY);
const schemeId = byId("heightmapSelectionColorScheme").value;
const scheme = getColorScheme(schemeId);
const renderOcean = byId("heightmapSelectionRenderOcean").checked;
const getHeight = height => (height < 20 ? (renderOcean ? height : 0) : height);
for (let i = 0; i < heights.length; i++) {
const color = scheme(1 - getHeight(heights[i]) / 100);
const {r, g, b} = d3.color(color);
const n = i * 4;
imageData.data[n] = r;
imageData.data[n + 1] = g;
imageData.data[n + 2] = b;
imageData.data[n + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL("image/png");
}
function generateHeightmap(id) {
HeightmapGenerator.resetHeights();
const heights = HeightmapGenerator.fromTemplate(id);
HeightmapGenerator.cleanup();
return heights;
}
function drawTemplatePreview(id) {
const heights = generateHeightmap(id);
const dataUrl = drawHeights(heights);
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
article.querySelector("img").src = dataUrl;
}
async function drawPrecreatedHeightmap(id) {
const heights = await HeightmapGenerator.fromPrecreated(id);
const dataUrl = drawHeights(heights);
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
article.querySelector("img").src = dataUrl;
}
function redrawAll() {
const articles = byId("heightmapSelection").querySelectorAll(`article`);
for (const article of articles) {
const {id, seed} = article.dataset;
Math.random = aleaPRNG(seed);
const isTemplate = id in HeightmapTemplates;
if (isTemplate) drawTemplatePreview(id);
else drawPrecreatedHeightmap(id);
}
}
function confirmHeightmapEdit() {
const tool = this.dataset.tool;
confirmationDialog({
title: this.dataset.tip,
message: "Opening the tool will erase the current map. Are you sure you want to proceed?",
confirm: "Continue",
onConfirm: () => editHeightmap({mode: "erase", tool})
});
}