Tileset creator done

This commit is contained in:
2026-02-16 12:00:55 -06:00
parent 99d030003c
commit 2b1a3323a8

View File

@@ -30,6 +30,26 @@
<div> <div>
<h2>Tileset Settings</h2> <h2>Tileset Settings</h2>
<div>
<button data-load-tileset>Load Tileset</button>
</div>
<div>
Define tile count by:
<div>
<label>
Tile Size
<input type="radio" name="define-by" value="size" checked />
</label>
<label>
Tile Count
<input type="radio" name="define-by" value="count" />
</label>
</div>
</div>
<div data-tile-sizes>
<div> <div>
<label>Tile Width:</label> <label>Tile Width:</label>
<input type="number" value="8" data-tile-width min="1" step="1" /> <input type="number" value="8" data-tile-width min="1" step="1" />
@@ -38,6 +58,8 @@
<label>Tile Height:</label> <label>Tile Height:</label>
<input type="number" value="8" data-tile-height min="1" step="1" /> <input type="number" value="8" data-tile-height min="1" step="1" />
</div> </div>
</div>
<div data-tile-counts style="display: none;">
<div> <div>
<label>Column Count:</label> <label>Column Count:</label>
<input type="number" value="10" data-column-count min="1" step="1" /> <input type="number" value="10" data-column-count min="1" step="1" />
@@ -48,6 +70,16 @@
</div> </div>
</div> </div>
<div>
<label>Unused Space on Right of Texture:</label>
<input type="number" value="0" data-right min="0" step="1" />
</div>
<div>
<label>Unused Space on Bottom of Texture:</label>
<input type="number" value="0" data-bottom min="0" step="1" />
</div>
</div>
<div> <div>
<h2>Preview</h2> <h2>Preview</h2>
<div> <div>
@@ -80,9 +112,15 @@
<div> <div>
<button data-tileset-download>Download Tileset</button> <button data-tileset-download>Download Tileset</button>
</div> </div>
</div>
</body> </body>
<script type="text/javascript"> <script type="text/javascript">
// Element selectors
const elDefineBySize = document.querySelector('input[name="define-by"][value="size"]');
const elDefineByCount = document.querySelector('input[name="define-by"][value="count"]');
const elTileSizes = document.querySelector('[data-tile-sizes]');
const elTileCounts = document.querySelector('[data-tile-counts]');
const elTileWidth = document.querySelector('[data-tile-width]'); const elTileWidth = document.querySelector('[data-tile-width]');
const elTileHeight = document.querySelector('[data-tile-height]'); const elTileHeight = document.querySelector('[data-tile-height]');
const elColumnCount = document.querySelector('[data-column-count]'); const elColumnCount = document.querySelector('[data-column-count]');
@@ -92,6 +130,8 @@
const elOutputInformation = document.querySelector('[data-output-information]'); const elOutputInformation = document.querySelector('[data-output-information]');
const elOutputPreview = document.querySelector('[data-output-preview]'); const elOutputPreview = document.querySelector('[data-output-preview]');
const elScale = document.querySelector('[data-indexed-preview-scale]'); const elScale = document.querySelector('[data-indexed-preview-scale]');
const elRight = document.querySelector('[data-right]');
const elBottom = document.querySelector('[data-bottom]');
const btnDownloadTileset = document.querySelector('[data-tileset-download]'); const btnDownloadTileset = document.querySelector('[data-tileset-download]');
const btnBackgroundWhite = document.querySelector('[data-page-bg-white]'); const btnBackgroundWhite = document.querySelector('[data-page-bg-white]');
const btnBackgroundTransparent = document.querySelector('[data-page-bg-transparent]'); const btnBackgroundTransparent = document.querySelector('[data-page-bg-transparent]');
@@ -99,130 +139,181 @@
const btnBackgroundMagenta = document.querySelector('[data-page-bg-magenta]'); const btnBackgroundMagenta = document.querySelector('[data-page-bg-magenta]');
const btnBackgroundBlue = document.querySelector('[data-page-bg-blue]'); const btnBackgroundBlue = document.querySelector('[data-page-bg-blue]');
const btnBackgroundGreen = document.querySelector('[data-page-bg-green]'); const btnBackgroundGreen = document.querySelector('[data-page-bg-green]');
const btnDownloadPalette = document.querySelector('[data-palette-download]'); const btnLoadTileset = document.querySelector('[data-load-tileset]');
const btnDownloadImage = document.querySelector('[data-indexed-download]');
// State
let imageWidth = 0; let imageWidth = 0;
let imageHeight = 0; let imageHeight = 0;
let image = null; let pixels = null;
let hoveredX = -1; let hoveredX = -1;
let hoveredY = -1; let hoveredY = -1;
const updatePreview = () => { const getValues = () => {
if(!image) return; if(!pixels) return null;
let tileWidth, tileHeight, columnCount, rowCount, right, bottom;
if(elDefineBySize.checked) {
console.log('Defining by size');
tileWidth = parseInt(elTileWidth.value) || 0;
tileHeight = parseInt(elTileHeight.value) || 0;
columnCount = Math.floor(imageWidth / tileWidth);
rowCount = Math.floor(imageHeight / tileHeight);
} else {
console.log('Defining by count');
columnCount = parseInt(elColumnCount.value) || 0;
rowCount = parseInt(elRowCount.value) || 0;
tileWidth = Math.floor(imageWidth / columnCount);
tileHeight = Math.floor(imageHeight / rowCount);
}
right = parseInt(elRight.value) || 0;
bottom = parseInt(elBottom.value) || 0;
const scale = parseInt(elScale.value) || 1; const scale = parseInt(elScale.value) || 1;
elOutputPreview.width = imageWidth * scale; const scaledWidth = imageWidth * scale;
elOutputPreview.height = imageHeight * scale; const scaledHeight = imageHeight * scale;
const scaledTileWidth = tileWidth * scale;
const scaledTileHeight = tileHeight * scale;
const scaledRight = right * scale;
const scaledBottom = bottom * scale;
const u0 = tileWidth / imageWidth - (right / imageWidth);
const v0 = tileHeight / imageHeight - (bottom / imageHeight);
const hoveredTileX = isNaN(hoveredX) || hoveredX < 0 ? 0 : hoveredX;
const hoveredTileY = isNaN(hoveredY) || hoveredY < 0 ? 0 : hoveredY;
const hoveredU0 = hoveredTileX * tileWidth / imageWidth;
const hoveredV0 = hoveredTileY * tileHeight / imageHeight;
const hoveredU1 = hoveredU0 + u0;
const hoveredV1 = hoveredV0 + v0;
const hoveredTileIndex = hoveredTileY * columnCount + hoveredTileX;
return {
tileWidth,
tileHeight,
columnCount,
rowCount,
right,
bottom,
scale,
scaledWidth,
scaledHeight,
scaledTileWidth,
scaledTileHeight,
scaledRight,
scaledBottom,
u0,
v0,
hoveredU0,
hoveredV0,
hoveredU1,
hoveredV1,
hoveredTileX,
hoveredTileY,
hoveredTileIndex,
}
}
const updatePreview = () => {
const v = getValues();
if(!v) return;
// console.log('Updating preview with values', v);
// Prepare canvas
elOutputPreview.width = v.scaledWidth;
elOutputPreview.height = v.scaledHeight;
const ctx = elOutputPreview.getContext('2d'); const ctx = elOutputPreview.getContext('2d');
ctx.clearRect(0, 0, elOutputPreview.width, elOutputPreview.height); ctx.clearRect(0, 0, elOutputPreview.width, elOutputPreview.height);
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
ctx.drawImage(image, 0, 0, elOutputPreview.width, elOutputPreview.height);
// Draw grid lines // Resize pixels
const tileWidth = parseInt(elTileWidth.value) || 0; const tempCanvas = document.createElement('canvas');
const tileHeight = parseInt(elTileHeight.value) || 0; tempCanvas.width = imageWidth;
const scaledTileWidth = tileWidth * scale; tempCanvas.height = imageHeight;
const scaledTileHeight = tileHeight * scale; const tempCtx = tempCanvas.getContext('2d');
const columnCount = parseInt(elColumnCount.value) || 0; const imageData = tempCtx.createImageData(imageWidth, imageHeight);
const rowCount = parseInt(elRowCount.value) || 0; imageData.data.set(pixels);
tempCtx.putImageData(imageData, 0, 0);
ctx.drawImage(tempCanvas, 0, 0, elOutputPreview.width, elOutputPreview.height);
// Draw blue overflow area for right and bottom cutoff
ctx.fillStyle = 'rgba(0,0,255,0.5)';
if(v.right > 0) {
ctx.fillRect(v.scaledWidth - v.scaledRight, 0, v.scaledRight, elOutputPreview.height);
}
if(v.bottom > 0) {
ctx.fillRect(0, v.scaledHeight - v.scaledBottom, elOutputPreview.width, v.scaledBottom);
}
// Draw red grid lines for tile boundaries
ctx.strokeStyle = 'rgba(255,0,0,1)'; ctx.strokeStyle = 'rgba(255,0,0,1)';
for(let x = scaledTileWidth; x < elOutputPreview.width; x += scaledTileWidth) { for (let x = v.scaledTileWidth; x < elOutputPreview.width; x += v.scaledTileWidth) {
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x, 0); ctx.moveTo(x, 0);
ctx.lineTo(x, elOutputPreview.height); ctx.lineTo(x, elOutputPreview.height);
ctx.stroke(); ctx.stroke();
} }
for(let y = scaledTileHeight; y < elOutputPreview.height; y += scaledTileHeight) { for (let y = v.scaledTileHeight; y < elOutputPreview.height; y += v.scaledTileHeight) {
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, y); ctx.moveTo(0, y);
ctx.lineTo(elOutputPreview.width, y); ctx.lineTo(elOutputPreview.width, y);
ctx.stroke(); ctx.stroke();
} }
const u0 = tileWidth / imageWidth;
const v0 = tileHeight / imageHeight;
const hoveredU0 = hoveredX != -1 && hoveredY != -1 ? (hoveredX * tileWidth / imageWidth) : 0;
const hoveredV0 = hoveredX != -1 && hoveredY != -1 ? (hoveredY * tileHeight / imageHeight) : 0;
const hoveredU1 = hoveredU0 + u0;
const hoveredV1 = hoveredV0 + v0;
elOutputInformation.value = [ elOutputInformation.value = [
hoveredX != -1 ? `Hovered Tile: ${hoveredX}, ${hoveredY} (${hoveredY * columnCount + hoveredX})` : 'Hovered Tile: None', v.hoveredX != -1 ? `Hovered Tile: ${v.hoveredTileX}, ${v.hoveredTileY} (${v.hoveredTileIndex})` : 'Hovered Tile: None',
hoveredX != -1 ? `Hovered UV: ${(hoveredU0).toFixed(4)}, ${(hoveredV0).toFixed(4)} -> ${(hoveredU1).toFixed(4)}, ${(hoveredV1).toFixed(4)}` : 'Hovered UV: None', v.hoveredX != -1 ? `Hovered UV: ${(v.hoveredU0).toFixed(4)}, ${(v.hoveredV0).toFixed(4)} -> ${(v.hoveredU1).toFixed(4)}, ${(v.hoveredV1).toFixed(4)}` : 'Hovered UV: None',
`Image Width: ${imageWidth}`, `Image Width: ${imageWidth}`,
`Image Height: ${imageHeight}`, `Image Height: ${imageHeight}`,
`Tile Width: ${elTileWidth.value}`, `Tile Width: ${v.tileWidth}`,
`Tile Height: ${elTileHeight.value}`, `Tile Height: ${v.tileHeight}`,
`Column Count: ${columnCount}`, `Column Count: ${v.columnCount}`,
`uv: ${u0.toFixed(4)}, ${v0.toFixed(4)}`, `uv: ${v.u0.toFixed(4)}, ${v.v0.toFixed(4)}`,
`Row Count: ${rowCount}`, `Row Count: ${v.rowCount}`,
`Tile count: ${columnCount * rowCount}`, `Tile count: ${v.columnCount * v.rowCount}`,
].join('\n'); ].join('\n');
} }
elTileWidth.addEventListener('input', () => { elTileWidth.addEventListener('input', updatePreview);
if(imageWidth) { elTileHeight.addEventListener('input', updatePreview);
const tileWidth = parseInt(elTileWidth.value); elColumnCount.addEventListener('input', updatePreview);
const columnCount = Math.floor(imageWidth / tileWidth); elRowCount.addEventListener('input', updatePreview);
elColumnCount.value = columnCount; elRight.addEventListener('input', updatePreview);
elBottom.addEventListener('input', updatePreview);
elScale.addEventListener('input', updatePreview);
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');
elDefineBySize.addEventListener('change', () => {
if (elDefineBySize.checked) {
elTileSizes.style.display = '';
elTileCounts.style.display = 'none';
} }
updatePreview(); updatePreview();
}); });
elTileHeight.addEventListener('input', () => { elDefineByCount.addEventListener('change', () => {
if(imageHeight) { if (elDefineByCount.checked) {
const tileHeight = parseInt(elTileHeight.value); elTileSizes.style.display = 'none';
const rowCount = Math.floor(imageHeight / tileHeight); elTileCounts.style.display = '';
elRowCount.value = rowCount;
} }
updatePreview(); updatePreview();
}); });
elColumnCount.addEventListener('input', () => {
if(!imageWidth) {
alert('Set an image first to calculate tile width from column count');
return;
}
const columnCount = parseInt(elColumnCount.value);
const tileWidth = Math.floor(imageWidth / columnCount);
elTileWidth.value = tileWidth;
updatePreview();
});
elRowCount.addEventListener('input', () => {
if(!imageHeight) {
alert('Set an image first to calculate tile height from row count');
return;
}
const rowCount = parseInt(elRowCount.value);
const tileHeight = Math.floor(imageHeight / rowCount);
elTileHeight.value = tileHeight;
updatePreview();
});
elScale.addEventListener('input', () => {
updatePreview();
});
elOutputPreview.addEventListener('mousemove', (e) => { elOutputPreview.addEventListener('mousemove', (e) => {
if(!image) return; const values = getValues();
if(!values) return;
const scale = parseInt(elScale.value) || 1;
const tileWidth = parseInt(elTileWidth.value) || 0;
const tileHeight = parseInt(elTileHeight.value) || 0;
const columnCount = parseInt(elColumnCount.value) || 0;
const rowCount = parseInt(elRowCount.value) || 0;
const rect = elOutputPreview.getBoundingClientRect(); const rect = elOutputPreview.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / scale); const x = Math.floor((e.clientX - rect.left) / values.scale);
const y = Math.floor((e.clientY - rect.top) / scale); const y = Math.floor((e.clientY - rect.top) / values.scale);
hoveredX = Math.floor(x / tileWidth); hoveredX = Math.floor(x / values.tileWidth);
hoveredY = Math.floor(y / tileHeight); hoveredY = Math.floor(y / values.tileHeight);
if(hoveredX < 0 || hoveredX >= values.columnCount || hoveredY < 0 || hoveredY >= values.rowCount) {
if(hoveredX < 0 || hoveredX >= columnCount || hoveredY < 0 || hoveredY >= rowCount) {
hoveredX = -1; hoveredX = -1;
hoveredY = -1; hoveredY = -1;
} }
@@ -239,6 +330,7 @@
// File // File
elFileInput.addEventListener('change', (e) => { elFileInput.addEventListener('change', (e) => {
elOutputError.style.display = 'none'; elOutputError.style.display = 'none';
pixels = null;
if (!elFileInput.files.length) { if (!elFileInput.files.length) {
elOutputError.textContent = 'No file selected'; elOutputError.textContent = 'No file selected';
@@ -247,57 +339,135 @@
} }
const file = elFileInput.files[0]; const file = elFileInput.files[0];
image = new Image();
if (file.name.endsWith('.dpt')) {
// Load DPT file
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const data = new Uint8Array(arrayBuffer);
if (data[0] !== 'D'.charCodeAt(0) || data[1] !== 'P'.charCodeAt(0) || data[2] !== 'T'.charCodeAt(0)) {
elOutputError.textContent = 'Invalid DPT file';
elOutputError.style.display = 'block';
return;
} else if (data[3] !== 0x01) {
elOutputError.textContent = 'Unsupported DPT version';
elOutputError.style.display = 'block';
return;
}
// Begin color indexes
const width = (
data[4] |
(data[5] << 8) |
(data[6] << 16) |
(data[7] << 24)
)
const height = (
data[8] |
(data[9] << 8) |
(data[10] << 16) |
(data[11] << 24)
);
imageWidth = width;
imageHeight = height;
if(data.length < 12 + width * height) {
elOutputError.textContent = 'Invalid DPT file: not enough pixel data';
elOutputError.style.display = 'block';
return;
}
const uniqueIndexes = [];
for (let i = 0; i < width * height; i++) {
const colorIndex = data[12 + i];
if (!uniqueIndexes.includes(colorIndex)) {
uniqueIndexes.push(colorIndex);
}
}
const adhocPalette = [];
for (let i = 0; i < uniqueIndexes.length; i++) {
const index = uniqueIndexes[i];
// Get the most different possible color for this index
const color = [
(index * 37) % 256,
(index * 61) % 256,
(index * 97) % 256,
255
];
adhocPalette[index] = color;
}
pixels = new Uint8Array(width * height * 4);
for (let i = 0; i < width * height; i++) {
const colorIndex = data[12 + i];
const color = adhocPalette[colorIndex];
pixels[i * 4] = color[0];
pixels[i * 4 + 1] = color[1];
pixels[i * 4 + 2] = color[2];
pixels[i * 4 + 3] = color[3];
}
updatePreview();
}
reader.onerror = () => {
elOutputError.textContent = 'Failed to read file';
elOutputError.style.display = 'block';
}
reader.readAsArrayBuffer(file);
} else {
const image = new Image();
image.onload = () => { image.onload = () => {
imageWidth = image.width; imageWidth = image.width;
imageHeight = image.height; imageHeight = image.height;
const tileWidth = parseInt(elTileWidth.value);
const tileHeight = parseInt(elTileHeight.value); // Pixels
const columnCount = Math.floor(imageWidth / tileWidth); const tempCanvas = document.createElement('canvas');
const rowCount = Math.floor(imageHeight / tileHeight); tempCanvas.width = imageWidth;
elColumnCount.value = columnCount; tempCanvas.height = imageHeight;
elRowCount.value = rowCount; const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(image, 0, 0);
const imageData = tempCtx.getImageData(0, 0, imageWidth, imageHeight);
pixels = imageData.data;
updatePreview(); updatePreview();
}; };
image.onerror = () => { image.onerror = () => {
image = null; pixels = null;
elOutputError.textContent = 'Failed to load image'; elOutputError.textContent = 'Failed to load image';
elOutputError.style.display = 'block'; elOutputError.style.display = 'block';
updatePreview(); updatePreview();
}; };
image.src = URL.createObjectURL(file); image.src = URL.createObjectURL(file);
}
}); });
btnDownloadTileset.addEventListener('click', () => { btnDownloadTileset.addEventListener('click', () => {
if(!image) { const v = getValues();
alert('No image loaded'); if(!v) {
alert('No valid tileset to download');
return; return;
} }
const tileWidth = parseInt(elTileWidth.value); // Header: DTF0, tileWidth, tileHeight, columnCount, rowCount, right, bottom, u0, v0
const tileHeight = parseInt(elTileHeight.value);
const columnCount = parseInt(elColumnCount.value);
const rowCount = parseInt(elRowCount.value);
const u0 = tileWidth / imageWidth;
const v0 = tileHeight / imageHeight;
const tileCount = columnCount * rowCount;
const headerBytes = new Uint8Array([ const headerBytes = new Uint8Array([
'D'.charCodeAt(0), // Dusk 'D'.charCodeAt(0), // Dusk
'T'.charCodeAt(0), // Tileset 'T'.charCodeAt(0), // Tileset
'F'.charCodeAt(0), // File/Format 'F'.charCodeAt(0), // File/Format
0x00, // version 0x00, // version
tileWidth & 0xFF,// Tile width (uint16_t) v.tileWidth & 0xFF, (v.tileWidth >> 8) & 0xFF,
(tileWidth >> 8) & 0xFF, v.tileHeight & 0xFF, (v.tileHeight >> 8) & 0xFF,
tileHeight & 0xFF,// Tile height (uint16_t) v.columnCount & 0xFF, (v.columnCount >> 8) & 0xFF,
(tileHeight >> 8) & 0xFF, v.rowCount & 0xFF, (v.rowCount >> 8) & 0xFF,
columnCount & 0xFF,// Column count (uint16_t) v.right & 0xFF, (v.right >> 8) & 0xFF,
(columnCount >> 8) & 0xFF, v.bottom & 0xFF, (v.bottom >> 8) & 0xFF,
rowCount & 0xFF,// Row count (uint16_t) ...new Uint8Array(new Float32Array([v.u0]).buffer),
(rowCount >> 8) & 0xFF, ...new Uint8Array(new Float32Array([v.v0]).buffer),
// Float32_t UV step (u0, v0)
...new Uint8Array(new Float32Array([u0]).buffer),
...new Uint8Array(new Float32Array([v0]).buffer),
]); ]);
// Download file // Download file
@@ -307,25 +477,65 @@
a.href = url; a.href = url;
a.download = 'tileset.dtf'; a.download = 'tileset.dtf';
a.click(); a.click();
URL.revokeObjectURL(url);
}); });
btnBackgroundWhite.addEventListener('click', () => { btnLoadTileset.addEventListener('click', () => {
document.body.style.background = 'white'; // Browse for file.
const input = document.createElement('input');
input.type = 'file';
input.accept = '.dtf';
input.addEventListener('change', (e) => {
const files = e?.target?.files;
if (!files || !files.length || !files[0]) {
alert('No file selected');
return;
}
const file = files[0];
if (!file.name.endsWith('.dtf')) {
alert('Invalid file type. Please select a .dtf file.');
return;
}
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const data = new Uint8Array(arrayBuffer);
if (data[0] !== 'D'.charCodeAt(0) || data[1] !== 'T'.charCodeAt(0) || data[2] !== 'F'.charCodeAt(0)) {
alert('Invalid DTF file');
return;
}
if (data[3] !== 0x00) {
alert('Unsupported DTF version');
return;
}
const tileWidth = data[4] | (data[5] << 8);
const tileHeight = data[6] | (data[7] << 8);
const columnCount = data[8] | (data[9] << 8);
const rowCount = data[10] | (data[11] << 8);
const right = data[12] | (data[13] << 8);
const bottom = data[14] | (data[15] << 8);
// Switch to using size definition
elDefineBySize.checked = true;
elTileWidth.value = tileWidth;
elTileHeight.value = tileHeight;
elTileSizes.style.display = '';
elTileCounts.style.display = 'none';
elRight.value = right;
elBottom.value = bottom;
updatePreview();
};
reader.onerror = () => {
alert('Failed to read file');
};
reader.readAsArrayBuffer(file);
}); });
btnBackgroundTransparent.addEventListener('click', () => { input.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';
}); });
// Init // Init