Mostly nuking old system
This commit is contained in:
223
tools/tile-joiner.html
Normal file
223
tools/tile-joiner.html
Normal file
@@ -0,0 +1,223 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user