feat: improve tiles export UX

This commit is contained in:
Azgaar 2024-03-17 02:03:56 +01:00
parent 72b6314d34
commit 7f587400ec
4 changed files with 102 additions and 86 deletions

View file

@ -6032,7 +6032,7 @@
</div>
<div id="exportToPngTilesScreen" style="display: none" class="dialog">
<p>Map will be split into tiles and downloaded as a single zip file. Avoid saving to big images</p>
<p>Map will be split into tiles and downloaded as a single zip file. Avoid saving too large images</p>
<div data-tip="Number of columns" style="margin-bottom: 0.3em">
<div class="label">Columns:</div>
<input
@ -6040,7 +6040,7 @@
data-stored="tileCols"
type="range"
min="2"
max="20"
max="26"
value="8"
style="width: 10em"
/>
@ -6053,7 +6053,7 @@
data-stored="tileRows"
type="range"
min="2"
max="20"
max="26"
value="8"
style="width: 10em"
/>
@ -6076,7 +6076,7 @@
<div class="label">Total size:</div>
<div id="tileSize" style="display: inline-block">1000 x 1000 px</div>
</div>
<div id="tileStatus" style="background-color: #33333310; font-style: italic"></div>
<div id="tileStatus" style="font-style: italic"></div>
</div>
<div id="resampleDialog" style="display: none" class="dialog">
@ -8095,7 +8095,7 @@
<script defer src="modules/io/save.js?v=1.96.00"></script>
<script defer src="modules/io/load.js?v=1.97.02"></script>
<script defer src="modules/io/cloud.js?v=1.96.00"></script>
<script defer src="modules/io/export.js?v=1.96.00"></script>
<script defer src="modules/io/export.js?v=1.97.03"></script>
<!-- Web Components -->
<script defer src="components/fill-box.js"></script>

View file

@ -71,70 +71,94 @@ async function exportToJpeg() {
}
async function exportToPngTiles() {
return new Promise(async (resolve, reject) => {
// download schema
const urlSchema = await getMapURL("tiles", {debug: true, fullMap: true});
await import("../../libs/jszip.min.js");
const zip = new window.JSZip();
const status = byId("tileStatus");
status.innerHTML = "Preparing files...";
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = graphWidth;
canvas.height = graphHeight;
const urlSchema = await getMapURL("tiles", {debug: true, fullMap: true});
await import("../../libs/jszip.min.js");
const zip = new window.JSZip();
const imgSchema = new Image();
imgSchema.src = urlSchema;
imgSchema.onload = function () {
ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => zip.file(`fmg_tile_schema.png`, blob));
};
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = graphWidth;
canvas.height = graphHeight;
// download tiles
const url = await getMapURL("tiles", {fullMap: true});
const tilesX = +document.getElementById("tileColsInput").value;
const tilesY = +document.getElementById("tileRowsInput").value;
const scale = +document.getElementById("tileScaleInput").value;
const imgSchema = new Image();
imgSchema.src = urlSchema;
await loadImage(imgSchema);
const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0;
const tolesTotal = tilesX * tilesY;
status.innerHTML = "Drawing schema...";
ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height);
const blob = await canvasToBlob(canvas, "image/png");
ctx.clearRect(0, 0, canvas.width, canvas.height);
zip.file("schema.png", blob);
const width = graphWidth * scale;
const height = width * (tileH / tileW);
canvas.width = width;
canvas.height = height;
// download tiles
const url = await getMapURL("tiles", {fullMap: true});
const tilesX = +byId("tileColsInput").value;
const tilesY = +byId("tileRowsInput").value;
const scale = +byId("tileScaleInput").value;
const tolesTotal = tilesX * tilesY;
let loaded = 0;
const img = new Image();
img.src = url;
img.onload = function () {
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
const name = `fmg_tile_${i}.png`;
canvas.toBlob(blob => {
zip.file(name, blob);
loaded += 1;
if (loaded === tolesTotal) return downloadZip();
});
}
}
};
const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0;
function downloadZip() {
const name = `${getFileName()}.zip`;
zip.generateAsync({type: "blob"}).then(blob => {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = name;
link.click();
link.remove();
const width = graphWidth * scale;
const height = width * (tileH / tileW);
canvas.width = width;
canvas.height = height;
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
resolve(true);
});
const img = new Image();
img.src = url;
await loadImage(img);
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (let y = 0, row = 0, id = 1; y + tileH <= graphHeight; y += tileH, row++) {
const rowName = alphabet[row % alphabet.length];
for (let x = 0, cell = 1; x + tileW <= graphWidth; x += tileW, cell++, id++) {
status.innerHTML = `Drawing tile ${rowName}${cell} (${id} of ${tolesTotal})...`;
ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
const blob = await canvasToBlob(canvas, "image/png");
ctx.clearRect(0, 0, canvas.width, canvas.height);
zip.file(`${rowName}${cell}.png`, blob);
}
}
status.innerHTML = "Zipping files...";
zip.generateAsync({type: "blob"}).then(blob => {
status.innerHTML = "Downloading the archive...";
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = getFileName() + ".zip";
link.click();
link.remove();
status.innerHTML = 'Done. Check .zip file in "Downloads" (crtl + J)';
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
});
// promisified img.onload
function loadImage(img) {
return new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = err => reject(err);
});
}
// promisified canvas.toBlob
function canvasToBlob(canvas, mimeType, qualityArgument = 1) {
return new Promise((resolve, reject) => {
canvas.toBlob(
blob => {
if (blob) resolve(blob);
else reject(new Error("Canvas toBlob() error"));
},
mimeType,
qualityArgument
);
});
}
}
// parse map svg to object url
@ -148,14 +172,14 @@ async function getMapURL(type, options) {
fullMap = false
} = options || {};
const cloneEl = document.getElementById("map").cloneNode(true); // clone svg
const cloneEl = byId("map").cloneNode(true); // clone svg
cloneEl.id = "fantasyMap";
document.body.appendChild(cloneEl);
const clone = d3.select(cloneEl);
if (!debug) clone.select("#debug")?.remove();
const cloneDefs = cloneEl.getElementsByTagName("defs")[0];
const svgDefs = document.getElementById("defElements");
const svgDefs = byId("defElements");
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
if (isFirefox && type === "mesh") clone.select("#oceanPattern")?.remove();
@ -218,7 +242,7 @@ async function getMapURL(type, options) {
.forEach(el => {
const href = el.getAttribute("href") || el.getAttribute("xlink:href");
if (!href) return;
const emblem = document.getElementById(href.slice(1));
const emblem = byId(href.slice(1));
if (emblem) cloneDefs.append(emblem.cloneNode(true));
});
} else {

View file

@ -867,11 +867,9 @@ byId("mapToLoad").addEventListener("change", function () {
});
function openExportToPngTiles() {
byId("tileStatus").innerHTML = "";
closeDialogs();
updateTilesOptions();
const status = byId("tileStatus");
status.innerHTML = "";
let loading = null;
const inputs = byId("exportToPngTilesScreen").querySelectorAll("input");
inputs.forEach(input => input.addEventListener("input", updateTilesOptions));
@ -881,16 +879,7 @@ function openExportToPngTiles() {
title: "Download tiles",
width: "23em",
buttons: {
Download: function () {
status.innerHTML = "Preparing for download...";
setTimeout(() => (status.innerHTML = "Downloading. It may take some time."), 1000);
loading = setInterval(() => (status.innerHTML += "."), 1000);
exportToPngTiles().then(() => {
clearInterval(loading);
status.innerHTML = /* html */ `Done. Check file in "Downloads" (crtl + J)`;
setTimeout(() => (status.innerHTML = ""), 8000);
});
},
Download: () => exportToPngTiles(),
Cancel: function () {
$(this).dialog("close");
}
@ -898,7 +887,6 @@ function openExportToPngTiles() {
close: () => {
inputs.forEach(input => input.removeEventListener("input", updateTilesOptions));
debug.selectAll("*").remove();
clearInterval(loading);
}
});
}
@ -928,18 +916,22 @@ function updateTilesOptions() {
const labels = [];
const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0;
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (let y = 0, row = 0; y + tileH <= graphHeight; y += tileH, row++) {
for (let x = 0, column = 1; x + tileW <= graphWidth; x += tileW, column++) {
rects.push(`<rect x=${x} y=${y} width=${tileW} height=${tileH} />`);
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${i}</text>`);
const label = alphabet[row % alphabet.length] + column;
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${label}</text>`);
}
}
const rectsG = "<g fill='none' stroke='#000'>" + rects.join("") + "</g>";
const labelsG =
"<g fill='#000' stroke='none' text-anchor='middle' dominant-baseline='central' font-size='24px'>" +
labels.join("") +
"</g>";
debug.html(rectsG + labelsG);
debug.html(`
<g fill='none' stroke='#000'>${rects.join("")}</g>
<g fill='#000' stroke='none' text-anchor='middle' dominant-baseline='central' font-size='18px'>${labels.join(
""
)}</g>
`);
}
// View mode

View file

@ -1,7 +1,7 @@
"use strict";
// version and caching control
const version = "1.97.02"; // generator version, update each time
const version = "1.97.03"; // generator version, update each time
{
document.title += " v" + version;