Files
dusk/tools/tile-joiner.html
Dominic Masters e5e8c49f6c
Some checks failed
Build Dusk / build-linux (push) Failing after 1m24s
Build Dusk / run-tests (push) Failing after 1m17s
Build Dusk / build-psp (push) Failing after 1m34s
Build Dusk / build-dolphin (push) Failing after 2m5s
Mostly nuking old system
2026-02-13 19:13:26 -06:00

223 lines
7.3 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dusk Tools / Tile Joiner</title>
<style type="text/css">
* {
box-sizing: border-box;
}
body {
font-size: 16px;
}
canvas {
image-rendering: pixelated;
}
</style>
</head>
<body>
<h1>Dusk Tile Joiner</h1>
<p>
Joins multiple tiles together into a single tileset image. Optimizing and
allowing it to work on multiple platforms. Output width and height will
always be a power of two. This tool will take a while to process images,
since all images need to be loaded into memory.
</p>
<div>
<div>
<input type="file" data-file-input multiple />
</div>
<p data-file-error style="color:red;display:none;"></p>
<textarea readonly data-input-information rows="10" style="width:500px"></textarea>
</div>
<div>
<h2>Settings</h2>
<div>
<label>
Preview Background:
<button data-page-bg-white>White</button>
<button data-page-bg-transparent>Black</button>
<button data-page-bg-checkerboard>Checkerboard</button>
<button data-page-bg-magenta>Magenta</button>
<button data-page-bg-blue>Blue</button>
<button data-page-bg-green>Green</button>
</label>
</div>
</div>
<div>
<h2>Joined Preview</h2>
<div>
<button data-download>Download Joined Image</button>
</div>
<canvas data-preview-canvas style="border:1px solid black;"></canvas>
</div>
</body>
<script type="text/javascript">
const elFileInput = document.querySelector('[data-file-input]');
const elFileError = document.querySelector('[data-file-error]');
const elInputInformation = document.querySelector('[data-input-information]');
const elPreviewCanvas = document.querySelector('[data-preview-canvas]');
const btnDownload = document.querySelector('[data-download]');
const btnBackgroundWhite = document.querySelector('[data-page-bg-white]');
const btnBackgroundTransparent = document.querySelector('[data-page-bg-transparent]');
const btnBackgroundCheckerboard = document.querySelector('[data-page-bg-checkerboard]');
const btnBackgroundMagenta = document.querySelector('[data-page-bg-magenta]');
const btnBackgroundBlue = document.querySelector('[data-page-bg-blue]');
const btnBackgroundGreen = document.querySelector('[data-page-bg-green]');
let images = {};
// Methods
const nextPowerOfTwo = n => {
if(n <= 0) return 1;
return 2 ** Math.ceil(Math.log2(n));
}
const onBadImages = error => {
elInputInformation.value = '';
elFileError.style.display = 'block';
elFileError.textContent = error;
}
const onImages = () => {
if(!images || Object.keys(images).length <= 1) {
return onBadImages('Please select 2 or more image images.');
}
elFileError.style.display = 'none';
let strInfo = `Selected ${Object.keys(images).length} images:\n`;
for(const [name, img] of Object.entries(images)) {
strInfo += `- ${name}: ${img.width}x${img.height}\n`;
}
elInputInformation.value = strInfo;
// Determine output width and height to pack images together, must be a
// power of two. If all images share a given axis (width/height) and that
// axis is already a power of two, use that.
const firstWidth = Object.values(images)[0].width;
const firstHeight = Object.values(images)[0].height;
let allImagesShareWidth = Object.values(images).every(img => img.width === firstWidth);
let allImagesShareHeight = Object.values(images).every(img => img.height === firstHeight);
let outputHeight, outputWidth;
if(allImagesShareWidth && nextPowerOfTwo(firstWidth) === firstWidth) {
outputWidth = firstWidth;
outputHeight = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.height, 0));
} else if(allImagesShareHeight && nextPowerOfTwo(firstHeight) === firstHeight) {
outputHeight = firstHeight;
outputWidth = nextPowerOfTwo(Object.values(images).reduce((sum, img) => sum + img.width, 0));
} else {
onBadImages('All images must share the same width or height, and that dimension must be a power of two.');
}
// Update preview
elPreviewCanvas.width = outputWidth;
elPreviewCanvas.height = outputHeight;
const ctx = elPreviewCanvas.getContext('2d');
let currentX = 0;
let currentY = 0;
for(const img of Object.values(images)) {
ctx.drawImage(img, currentX, currentY);
if(allImagesShareWidth) {
currentY += img.height;
} else {
currentX += img.width;
}
}
}
const onFiles = async files => {
images = {};
if(!files || files.length <= 1) {
return onBadImages('Please select 2 or more image files.');
}
let strError = '';
for(const file of files) {
if(!file.type.startsWith('image/')) {
strError += `File is not an image: ${file.name}\n`;
}
}
if(strError) {
return onBadImages(strError);
}
try {
const fileImages = await Promise.all(Array.from(files).map(file => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = event => {
const img = new Image();
img.onload = () => {
images[file.name] = img;
resolve();
}
img.onerror = () => {
reject(`Failed to load image: ${file.name}`);
}
img.src = event.target.result;
}
reader.onerror = () => {
reject(`Failed to read file: ${file.name}`);
}
reader.readAsDataURL(file);
});
}));
} catch(error) {
return onBadImages(error);
}
onImages();
}
// Listeners
elFileInput.addEventListener('change', event => {
onFiles(event?.target?.files);
});
btnBackgroundWhite.addEventListener('click', () => {
document.body.style.background = 'white';
});
btnBackgroundTransparent.addEventListener('click', () => {
document.body.style.background = 'black';
});
btnBackgroundCheckerboard.addEventListener('click', () => {
document.body.style.background = 'repeating-conic-gradient(#ccc 0% 25%, #eee 0% 50%) 50% / 20px 20px';
});
btnBackgroundMagenta.addEventListener('click', () => {
document.body.style.background = 'magenta';
});
btnBackgroundBlue.addEventListener('click', () => {
document.body.style.background = 'blue';
});
btnBackgroundGreen.addEventListener('click', () => {
document.body.style.background = 'green';
});
btnDownload.addEventListener('click', () => {
if(!images || Object.keys(images).length <= 1) {
return onBadImages('Please select 2 or more image files before downloading.');
}
const link = document.createElement('a');
link.download = 'joined.png';
link.href = elPreviewCanvas.toDataURL();
link.click();
});
btnBackgroundCheckerboard.click();
</script>
</html>