<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tile Sheet Editor</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
@media (max-width: 768px) {
.header {
padding: 20px;
}
.header h1 {
font-size: 1.8rem;
}
.header p {
font-size: 1rem;
}
}
.main-content {
display: flex;
min-height: 600px;
}
.sidebar {
width: 300px;
background: #f8f9fa;
border-right: 1px solid #e9ecef;
padding: 20px;
overflow-y: auto;
}
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid #e9ecef;
padding: 15px;
max-height: 300px;
}
}
.image-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.image-item {
display: flex;
align-items: center;
padding: 10px;
background: white;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.image-item:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
border-color: #667eea;
}
.image-item.active {
border-color: #667eea;
background: #f0f4ff;
}
.image-item img {
width: 60px;
height: 60px;
object-fit: contain;
border-radius: 5px;
margin-right: 10px;
background: #f8f9fa;
border: 1px solid #e9ecef;
}
.image-name {
font-weight: 500;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
.image-preview {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.preview-content {
position: relative;
max-width: 90vw;
max-height: 90vh;
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
}
.preview-title {
font-size: 1.2rem;
font-weight: 600;
color: #333;
}
.preview-close {
background: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.preview-close:hover {
background: #c82333;
}
.preview-canvas-container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
max-height: 70vh;
overflow: auto;
border-radius: 8px;
background: #f8f9fa;
}
.preview-canvas {
border: 2px solid #ddd;
border-radius: 5px;
background: white;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.preview-info {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
font-size: 12px;
color: #666;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.editor-area {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
}
@media (max-width: 768px) {
.editor-area {
padding: 15px;
}
}
.editor-header {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #e9ecef;
}
@media (max-width: 768px) {
.editor-header {
margin-bottom: 15px;
padding-bottom: 15px;
}
}
.editor-title {
font-size: 1.5rem;
color: #333;
margin-bottom: 10px;
}
@media (max-width: 768px) {
.editor-title {
font-size: 1.2rem;
margin-bottom: 8px;
}
}
.controls {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 15px;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
}
.control-group label {
font-weight: 500;
color: #555;
min-width: 80px;
font-size: 14px;
}
@media (max-width: 768px) {
.controls {
gap: 10px;
}
.control-group {
min-width: 100%;
justify-content: space-between;
margin-bottom: 8px;
}
.control-group label {
min-width: auto;
flex: 1;
font-size: 13px;
}
}
input[type="number"] {
width: 80px;
padding: 8px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s ease;
}
input[type="number"]:focus {
outline: none;
border-color: #667eea;
}
@media (max-width: 768px) {
input[type="number"] {
width: 70px;
padding: 6px;
font-size: 13px;
}
}
.btn {
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
margin-right: 10px;
margin-bottom: 8px;
font-size: 14px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
@media (max-width: 768px) {
.btn {
padding: 8px 16px;
font-size: 13px;
margin-right: 8px;
margin-bottom: 8px;
flex: 1;
min-width: calc(50% - 4px);
}
.btn:last-child {
margin-right: 0;
}
}
@media (max-width: 480px) {
.btn {
min-width: 100%;
margin-right: 0;
}
}
.btn.success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
}
.btn.warning {
background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
color: #333;
}
.selected-tiles-section {
margin-bottom: 15px;
}
.selected-tiles-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
min-height: 30px;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
border: 2px dashed #ddd;
}
@media (max-width: 768px) {
.selected-tiles-list {
gap: 6px;
padding: 8px;
min-height: 40px;
}
}
.tile-tag {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
}
.tile-tag .remove {
cursor: pointer;
font-weight: bold;
opacity: 0.7;
}
.tile-tag .remove:hover {
opacity: 1;
}
.canvas-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
position: relative;
overflow: auto;
}
@media (max-width: 768px) {
.canvas-container {
padding: 10px;
min-height: 400px;
}
}
.tile-grid-container {
position: relative;
display: inline-block;
border: 2px solid #ddd;
border-radius: 8px;
background: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.tile-image {
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.tile-overlay {
position: absolute;
top: 0;
left: 0;
display: grid;
cursor: crosshair;
}
.tile-cell {
position: relative;
border-right: 1px solid rgba(0, 255, 0, 0.5);
border-bottom: 1px solid rgba(0, 255, 0, 0.5);
box-sizing: border-box;
}
.tile-cell:nth-child(-n + var(--grid-cols)) {
border-top: 1px solid rgba(0, 255, 0, 0.5);
}
.tile-cell:nth-child(var(--grid-cols) + 1) ~ .tile-cell {
border-top: 1px solid rgba(0, 255, 0, 0.5);
}
.tile-cell:nth-child(n + var(--grid-cols) * (var(--grid-rows) - 1) + 1) {
border-bottom: none;
}
.tile-cell:nth-child(var(--grid-cols)) {
border-right: none;
}
.tile-cell.selected {
background: rgba(255, 0, 0, 0.3);
outline: 2px solid #ff0000;
box-sizing: border-box;
}
.tile-info {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px;
border-radius: 5px;
font-size: 12px;
display: none;
z-index: 10;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.loading::before {
content: '';
display: inline-block;
width: 30px;
height: 30px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-images {
text-align: center;
padding: 40px;
color: #666;
}
.json-output {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.json-output h4 {
margin-bottom: 10px;
color: #333;
}
.json-output pre {
background: #fff;
padding: 15px;
border-radius: 5px;
border: 1px solid #ddd;
font-size: 12px;
overflow-x: auto;
max-height: 300px;
overflow-y: auto;
}
@media (max-width: 768px) {
.tile-grid-container {
max-width: calc(100vw - 60px);
max-height: calc(100vh - 400px);
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎮 Tile Sheet Editor</h1>
<p>Select tiles and generate JSON configuration</p>
</div>
<div class="main-content">
<div class="sidebar">
<h3 style="margin-bottom: 15px; color: #333;">Available Images</h3>
<div style="margin-bottom: 20px;">
<input type="file" id="fileInput" accept="image/*" style="display: none;" multiple>
<button class="btn" onclick="document.getElementById('fileInput').click()" style="width: 100%; margin-bottom: 10px;">
📁 Add Tile Sheet(s)
</button>
<div id="uploadStatus" style="font-size: 12px; color: #666; text-align: center;"></div>
</div>
<div class="image-list" id="imageList">
<div class="loading">Loading images...</div>
</div>
</div>
<div class="editor-area">
<div class="editor-header">
<div class="editor-title" id="editorTitle">Select an image to start editing</div>
<div class="controls">
<div class="control-group">
<label>Tile Width:</label>
<input type="number" id="tileWidth" value="32" min="8" step="8">
</div>
<div class="control-group">
<label>Tile Height:</label>
<input type="number" id="tileHeight" value="32" min="8" step="8">
</div>
<div class="control-group">
<label>H Spacing:</label>
<input type="number" id="horizontalSpacing" value="0" min="0" max="50">
</div>
<div class="control-group">
<label>V Spacing:</label>
<input type="number" id="verticalSpacing" value="0" min="0" max="50">
</div>
<div class="control-group">
<label>X Offset:</label>
<input type="number" id="xOffset" value="0" min="-50" max="50">
</div>
<div class="control-group">
<label>Y Offset:</label>
<input type="number" id="yOffset" value="0" min="-50" max="50">
</div>
<button class="btn" onclick="updateGrid()">Update Grid</button>
</div>
<div class="selected-tiles-section">
<h4>Selected Tiles:</h4>
<div class="selected-tiles-list" id="selectedTilesList">
<span style="color: #999; font-style: italic;">Click tiles to select them...</span>
</div>
<div class="controls">
<button class="btn success" onclick="generateJSON()">Generate JSON</button>
<button class="btn warning" onclick="clearSelection()">Clear Selection</button>
<button class="btn" onclick="downloadTile()">Download Selected Tile</button>
</div>
</div>
</div>
<div class="canvas-container">
<div class="tile-grid-container" id="tileGridContainer">
<img id="tileImage" class="tile-image" style="display: none;">
<div id="tileOverlay" class="tile-overlay" style="display: none;"></div>
</div>
<div class="tile-info" id="tileInfo"></div>
</div>
<div class="image-preview" id="imagePreview">
<div class="preview-content">
<div class="preview-header">
<div class="preview-title" id="previewTitle">Image Preview</div>
<button class="preview-close" onclick="closeImagePreview()">×</button>
</div>
<div class="preview-canvas-container">
<canvas id="previewCanvas" class="preview-canvas"></canvas>
</div>
<div class="preview-info" id="previewInfo">
<span>Click and drag to pan • Scroll to zoom</span>
</div>
</div>
</div>
<div class="json-output" id="jsonOutput" style="display: none;">
<h4>Generated JSON:</h4>
<pre id="jsonContent"></pre>
<button class="btn" onclick="copyJSON()">Copy to Clipboard</button>
<button class="btn" onclick="downloadJSON()">Download JSON</button>
</div>
</div>
</div>
</div>
<script>
let currentImage = null;
let currentImagePath = '';
let selectedTiles = new Set();
let lastSelectedTile = { x: -1, y: -1 };
let gridCols = 0;
let gridRows = 0;
// Load images from PHP script
async function loadImages() {
try {
const response = await fetch('get_images.php');
const result = await response.json();
const imageList = document.getElementById('imageList');
// Handle error response
if (result.error) {
imageList.innerHTML = `
<div class="no-images">
<strong>Error:</strong> ${result.error}<br>
<small>Tried paths: ${result.tried_paths ? result.tried_paths.join(', ') : 'N/A'}</small><br>
<small>Current dir: ${result.current_dir || 'N/A'}</small>
</div>
`;
return;
}
// Handle array of images
const images = Array.isArray(result) ? result : (result.images || []);
if (images.length === 0) {
imageList.innerHTML = '<div class="no-images">No images found in the images folder</div>';
return;
}
imageList.innerHTML = '';
images.forEach((imagePath, index) => {
const item = document.createElement('div');
item.className = 'image-item';
// Create thumbnail image with error handling
const img = document.createElement('img');
img.loading = 'lazy';
img.alt = imagePath;
// Try different path variations
const possiblePaths = [
imagePath,
'../' + imagePath,
'/' + imagePath,
imagePath.replace('../', ''),
'images/' + imagePath.split('/').pop()
];
let pathIndex = 0;
function tryNextPath() {
if (pathIndex < possiblePaths.length) {
img.src = possiblePaths[pathIndex];
pathIndex++;
} else {
// All paths failed, show placeholder
img.style.display = 'none';
const placeholder = document.createElement('div');
placeholder.style.cssText = `
width: 60px;
height: 60px;
background: #f0f0f0;
border: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #666;
margin-right: 10px;
border-radius: 5px;
`;
placeholder.textContent = 'No img';
item.insertBefore(placeholder, img);
}
}
img.onerror = tryNextPath;
img.onload = function() {
console.log('Image loaded successfully:', img.src);
};
// Add preview functionality
img.addEventListener('click', (e) => {
e.stopPropagation();
showImagePreview(img.src);
});
const nameDiv = document.createElement('div');
nameDiv.className = 'image-name';
nameDiv.textContent = imagePath.split('/').pop();
item.appendChild(img);
item.appendChild(nameDiv);
item.addEventListener('click', () => {
document.querySelectorAll('.image-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
loadImage(img.src);
});
imageList.appendChild(item);
// Start trying paths
tryNextPath();
});
// Auto-load first image
if (images.length > 0) {
setTimeout(() => {
const firstImg = document.querySelector('.image-item img');
if (firstImg && firstImg.complete && firstImg.naturalWidth > 0) {
document.querySelector('.image-item').click();
}
}, 1000);
}
} catch (error) {
console.error('Error loading images:', error);
document.getElementById('imageList').innerHTML = `
<div class="no-images">
Error loading images: ${error.message}<br>
<small>Check browser console for details</small>
</div>
`;
}
}
function loadImage(imagePath) {
const img = new Image();
img.onload = function() {
currentImage = img;
currentImagePath = imagePath;
document.getElementById('editorTitle').textContent = `Editing: ${imagePath.split('/').pop()}`;
const tileImg = document.getElementById('tileImage');
tileImg.src = imagePath;
tileImg.style.display = 'block';
tileImg.style.width = currentImage.width + 'px';
tileImg.style.height = currentImage.height + 'px';
// Log image dimensions for debugging
console.log(`Image loaded: ${currentImage.width}x${currentImage.height}px`);
// Clear selection when loading new image
selectedTiles.clear();
updateSelectedTilesList();
updateGrid();
};
img.onerror = function() {
console.error('Failed to load image:', imagePath);
alert('Failed to load image: ' + imagePath);
};
img.src = imagePath;
}
function updateGrid() {
if (!currentImage) return;
const tileWidth = parseInt(document.getElementById('tileWidth').value);
const tileHeight = parseInt(document.getElementById('tileHeight').value);
const hSpacing = parseInt(document.getElementById('horizontalSpacing').value);
const vSpacing = parseInt(document.getElementById('verticalSpacing').value);
const xOffset = parseInt(document.getElementById('xOffset').value);
const yOffset = parseInt(document.getElementById('yOffset').value);
// Validate tile sizes to be multiples of 8
if (tileWidth % 8 !== 0 || tileHeight % 8 !== 0) {
console.warn('Tile width and height should be multiples of 8');
document.getElementById('tileWidth').value = Math.round(tileWidth / 8) * 8 || 8;
document.getElementById('tileHeight').value = Math.round(tileHeight / 8) * 8 || 8;
return updateGrid(); // Recalculate with corrected values
}
// Calculate grid dimensions
const effectiveWidth = tileWidth + hSpacing;
const effectiveHeight = tileHeight + vSpacing;
gridCols = Math.floor((currentImage.width - xOffset) / effectiveWidth);
gridRows = Math.floor((currentImage.height - yOffset) / effectiveHeight);
// If no spacing, use direct division
if (hSpacing === 0) {
gridCols = Math.floor((currentImage.width - xOffset) / tileWidth);
}
if (vSpacing === 0) {
gridRows = Math.floor((currentImage.height - yOffset) / tileHeight);
}
// Log grid calculations for debugging
console.log(`Grid calculated: ${gridCols} cols x ${gridRows} rows`);
console.log(`Tile size: ${tileWidth}x${tileHeight}px, Spacing: ${hSpacing}x${vSpacing}px, Offset: ${xOffset}x${yOffset}px`);
// Calculate total tiled area including gaps but excluding trailing spacing
const totalWidth = gridCols * tileWidth + (gridCols > 0 ? (gridCols - 1) * hSpacing : 0);
const totalHeight = gridRows * tileHeight + (gridRows > 0 ? (gridRows - 1) * vSpacing : 0);
// Set overlay dimensions and position
const overlay = document.getElementById('tileOverlay');
overlay.style.width = totalWidth + 'px';
overlay.style.height = totalHeight + 'px';
overlay.style.marginLeft = xOffset + 'px';
overlay.style.marginTop = yOffset + 'px';
overlay.style.display = 'grid';
// Set grid template
overlay.style.gridTemplateColumns = `repeat(${gridCols}, ${tileWidth}px)`;
overlay.style.gridTemplateRows = `repeat(${gridRows}, ${tileHeight}px)`;
overlay.style.gap = `${vSpacing}px ${hSpacing}px`; // row-gap column-gap
overlay.style.setProperty('--grid-cols', gridCols);
overlay.style.setProperty('--grid-rows', gridRows);
// Log overlay dimensions for debugging
console.log(`Overlay size: ${totalWidth}x${totalHeight}px`);
// Clear existing cells
overlay.innerHTML = '';
// Create tile cells
for (let row = 0; row < gridRows; row++) {
for (let col = 0; col < gridCols; col++) {
const cell = document.createElement('div');
cell.className = 'tile-cell';
cell.dataset.col = col;
cell.dataset.row = row;
cell.dataset.key = `${col},${row}`;
// Check if selected
if (selectedTiles.has(`${col},${row}`)) {
cell.classList.add('selected');
}
// Add click event
cell.addEventListener('click', function(e) {
e.stopPropagation();
toggleTile(col, row, cell.dataset.key);
});
overlay.appendChild(cell);
}
}
}
function toggleTile(col, row, tileKey) {
if (selectedTiles.has(tileKey)) {
selectedTiles.delete(tileKey);
console.log(`Deselected tile: ${tileKey}`);
} else {
selectedTiles.add(tileKey);
console.log(`Selected tile: ${tileKey}`);
}
lastSelectedTile.x = col;
lastSelectedTile.y = row;
updateSelectedTilesList();
updateGrid(); // Rebuild to update selected class
// Show tile info
const tileWidth = parseInt(document.getElementById('tileWidth').value);
const tileHeight = parseInt(document.getElementById('tileHeight').value);
const hSpacing = parseInt(document.getElementById('horizontalSpacing').value);
const vSpacing = parseInt(document.getElementById('verticalSpacing').value);
const xOffset = parseInt(document.getElementById('xOffset').value);
const yOffset = parseInt(document.getElementById('yOffset').value);
const actualTileX = xOffset + col * (tileWidth + hSpacing);
const actualTileY = yOffset + row * (tileHeight + vSpacing);
const tileInfo = document.getElementById('tileInfo');
tileInfo.innerHTML = `
Tile: (${col}, ${row})<br>
Position: (${actualTileX}px, ${actualTileY}px)<br>
Size: ${tileWidth}x${tileHeight}px<br>
Spacing: H${hSpacing}px V${vSpacing}px<br>
Status: ${selectedTiles.has(tileKey) ? 'Selected' : 'Unselected'}
`;
tileInfo.style.display = 'block';
}
function updateSelectedTilesList() {
const container = document.getElementById('selectedTilesList');
if (selectedTiles.size === 0) {
container.innerHTML = '<span style="color: #999; font-style: italic;">Click tiles to select them...</span>';
return;
}
container.innerHTML = '';
Array.from(selectedTiles).sort().forEach(tileKey => {
const [x, y] = tileKey.split(',').map(Number);
const tag = document.createElement('span');
tag.className = 'tile-tag';
tag.innerHTML = `(${x},${y}) <span class="remove" onclick="removeTile('${tileKey}')">×</span>`;
container.appendChild(tag);
});
}
function removeTile(tileKey) {
selectedTiles.delete(tileKey);
updateSelectedTilesList();
updateGrid();
}
function clearSelection() {
selectedTiles.clear();
updateSelectedTilesList();
updateGrid();
}
// Event listeners for controls with validation
document.getElementById('tileWidth').addEventListener('change', function() {
let value = parseInt(this.value);
if (value < 8) value = 8;
this.value = Math.round(value / 8) * 8;
updateGrid();
});
document.getElementById('tileHeight').addEventListener('change', function() {
let value = parseInt(this.value);
if (value < 8) value = 8;
this.value = Math.round(value / 8) * 8;
updateGrid();
});
document.getElementById('horizontalSpacing').addEventListener('change', updateGrid);
document.getElementById('verticalSpacing').addEventListener('change', updateGrid);
document.getElementById('xOffset').addEventListener('change', updateGrid);
document.getElementById('yOffset').addEventListener('change', updateGrid);
// Hide info on mouse leave
document.getElementById('tileGridContainer').addEventListener('mouseleave', function() {
document.getElementById('tileInfo').style.display = 'none';
});
function generateJSON() {
if (selectedTiles.size === 0) {
alert('Please select at least one tile');
return;
}
const tileWidth = parseInt(document.getElementById('tileWidth').value);
const tileHeight = parseInt(document.getElementById('tileHeight').value);
const hSpacing = parseInt(document.getElementById('horizontalSpacing').value);
const vSpacing = parseInt(document.getElementById('verticalSpacing').value);
const xOffset = parseInt(document.getElementById('xOffset').value);
const yOffset = parseInt(document.getElementById('yOffset').value);
const tilesData = Array.from(selectedTiles).map(tileKey => {
const [x, y] = tileKey.split(',').map(Number);
const pixelX = xOffset + x * (tileWidth + hSpacing);
const pixelY = yOffset + y * (tileHeight + vSpacing);
return {
id: `tile_${x}_${y}`,
grid_position: { x, y },
pixel_position: {
x: pixelX,
y: pixelY
},
size: {
width: tileWidth,
height: tileHeight
}
};
});
const jsonData = {
source_image: `root/images/${currentImagePath.split('/').pop()}`,
image_dimensions: {
width: currentImage.width,
height: currentImage.height
},
tile_configuration: {
tile_size: {
width: tileWidth,
height: tileHeight
},
spacing: {
horizontal: hSpacing,
vertical: vSpacing
},
offset: {
x: xOffset,
y: yOffset
}
},
grid_dimensions: {
columns: gridCols,
rows: gridRows
},
selected_tiles: tilesData,
total_selected: tilesData.length,
generated_at: new Date().toISOString()
};
document.getElementById('jsonContent').textContent = JSON.stringify(jsonData, null, 2);
document.getElementById('jsonOutput').style.display = 'block';
// Scroll to JSON output
document.getElementById('jsonOutput').scrollIntoView({ behavior: 'smooth' });
}
function copyJSON() {
const jsonText = document.getElementById('jsonContent').textContent;
navigator.clipboard.writeText(jsonText).then(() => {
alert('JSON copied to clipboard!');
});
}
function downloadJSON() {
const jsonText = document.getElementById('jsonContent').textContent;
const blob = new Blob([jsonText], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `tilemap_${currentImagePath.split('/').pop().split('.')[0]}_${new Date().getTime()}.json`;
link.click();
URL.revokeObjectURL(url);
}
function downloadTile() {
if (lastSelectedTile.x < 0 || lastSelectedTile.y < 0) {
alert('Please select a tile first');
return;
}
const tileWidth = parseInt(document.getElementById('tileWidth').value);
const tileHeight = parseInt(document.getElementById('tileHeight').value);
const hSpacing = parseInt(document.getElementById('horizontalSpacing').value);
const vSpacing = parseInt(document.getElementById('verticalSpacing').value);
const xOffset = parseInt(document.getElementById('xOffset').value);
const yOffset = parseInt(document.getElementById('yOffset').value);
// Calculate the actual position of the tile in the image
const actualTileX = xOffset + lastSelectedTile.x * (tileWidth + hSpacing);
const actualTileY = yOffset + lastSelectedTile.y * (tileHeight + vSpacing);
// Create a temporary canvas for the tile
const tileCanvas = document.createElement('canvas');
const tileCtx = tileCanvas.getContext('2d');
tileCanvas.width = tileWidth;
tileCanvas.height = tileHeight;
// Draw the selected tile using the correct position calculation
tileCtx.drawImage(
currentImage,
actualTileX, actualTileY, tileWidth, tileHeight,
0, 0, tileWidth, tileHeight
);
// Download the tile
const link = document.createElement('a');
link.download = `tile_${lastSelectedTile.x}_${lastSelectedTile.y}.png`;
link.href = tileCanvas.toDataURL();
link.click();
}
// Load images when page loads
loadImages();
// Image preview functionality
let previewCanvas = document.getElementById('previewCanvas');
let previewCtx = previewCanvas.getContext('2d');
let previewImage = null;
let previewZoom = 1;
let previewOffsetX = 0;
let previewOffsetY = 0;
let isDragging = false;
let lastMouseX = 0;
let lastMouseY = 0;
function showImagePreview(imagePath) {
const img = new Image();
img.onload = function() {
previewImage = img;
// Reset zoom and position
previewZoom = 1;
previewOffsetX = 0;
previewOffsetY = 0;
// Set canvas size
const maxWidth = window.innerWidth * 0.7;
const maxHeight = window.innerHeight * 0.6;
let displayWidth = img.width;
let displayHeight = img.height;
// Scale down if too large
if (displayWidth > maxWidth || displayHeight > maxHeight) {
const scale = Math.min(maxWidth / displayWidth, maxHeight / displayHeight);
displayWidth *= scale;
displayHeight *= scale;
previewZoom = scale;
}
previewCanvas.width = displayWidth;
previewCanvas.height = displayHeight;
// Update title and info
document.getElementById('previewTitle').textContent = imagePath.split('/').pop();
document.getElementById('previewInfo').innerHTML = `
<span>Dimensions: ${img.width} × ${img.height}px</span>
<span>Zoom: ${Math.round(previewZoom * 100)}%</span>
<span>Click thumbnail to edit • Right-click to close</span>
`;
drawPreview();
document.getElementById('imagePreview').style.display = 'flex';
};
img.onerror = function() {
console.error('Failed to load preview image:', imagePath);
alert('Failed to load preview for: ' + imagePath.split('/').pop());
};
img.src = imagePath;
}
function drawPreview() {
if (!previewImage) return;
const tileWidth = parseInt(document.getElementById('tileWidth').value);
const tileHeight = parseInt(document.getElementById('tileHeight').value);
const hSpacing = parseInt(document.getElementById('horizontalSpacing').value);
const vSpacing = parseInt(document.getElementById('verticalSpacing').value);
const xOffset = parseInt(document.getElementById('xOffset').value);
const yOffset = parseInt(document.getElementById('yOffset').value);
previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height);
// Draw the image
previewCtx.save();
previewCtx.translate(previewOffsetX, previewOffsetY);
previewCtx.scale(previewZoom, previewZoom);
previewCtx.imageSmoothingEnabled = false;
previewCtx.drawImage(previewImage, 0, 0);
// Draw grid overlay with spacing and offset
const effectiveTileWidth = tileWidth + hSpacing;
const effectiveTileHeight = tileHeight + vSpacing;
const gridCols = Math.floor((previewImage.width - xOffset) / effectiveTileWidth);
const gridRows = Math.floor((previewImage.height - yOffset) / effectiveTileHeight);
previewCtx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
previewCtx.lineWidth = 1 / previewZoom;
// Draw tile boundaries
for (let row = 0; row < gridRows; row++) {
for (let col = 0; col < gridCols; col++) {
const x = xOffset + col * effectiveTileWidth;
const y = yOffset + row * effectiveTileHeight;
previewCtx.strokeRect(x, y, tileWidth, tileHeight);
}
}
previewCtx.restore();
}
function closeImagePreview() {
document.getElementById('imagePreview').style.display = 'none';
previewImage = null;
}
// Preview canvas event listeners
previewCanvas.addEventListener('wheel', function(e) {
e.preventDefault();
const rect = previewCanvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
const newZoom = Math.max(0.1, Math.min(5, previewZoom * zoomFactor));
// Zoom towards mouse position
previewOffsetX = mouseX - (mouseX - previewOffsetX) * (newZoom / previewZoom);
previewOffsetY = mouseY - (mouseY - previewOffsetY) * (newZoom / previewZoom);
previewZoom = newZoom;
// Update info
document.getElementById('previewInfo').innerHTML = `
<span>Dimensions: ${previewImage.width} × ${previewImage.height}px</span>
<span>Zoom: ${Math.round(previewZoom * 100)}%</span>
<span>Click thumbnail to edit • Right-click to close</span>
`;
drawPreview();
});
previewCanvas.addEventListener('mousedown', function(e) {
if (e.button === 2) { // Right click to close
closeImagePreview();
return;
}
isDragging = true;
lastMouseX = e.clientX;
lastMouseY = e.clientY;
previewCanvas.style.cursor = 'grabbing';
});
previewCanvas.addEventListener('mousemove', function(e) {
if (isDragging) {
const deltaX = e.clientX - lastMouseX;
const deltaY = e.clientY - lastMouseY;
previewOffsetX += deltaX;
previewOffsetY += deltaY;
lastMouseX = e.clientX;
lastMouseY = e.clientY;
drawPreview();
}
});
previewCanvas.addEventListener('mouseup', function() {
isDragging = false;
previewCanvas.style.cursor = 'grab';
});
previewCanvas.addEventListener('mouseleave', function() {
isDragging = false;
previewCanvas.style.cursor = 'grab';
});
previewCanvas.style.cursor = 'grab';
// Close preview on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeImagePreview();
}
});
// Close preview when clicking outside
document.getElementById('imagePreview').addEventListener('click', function(e) {
if (e.target === this) {
closeImagePreview();
}
});
// Handle file uploads
document.getElementById('fileInput').addEventListener('change', function(e) {
const files = e.target.files;
if (files.length === 0) return;
uploadFiles(files);
});
async function uploadFiles(files) {
const uploadStatus = document.getElementById('uploadStatus');
uploadStatus.textContent = `Uploading ${files.length} file(s)...`;
uploadStatus.style.color = '#667eea';
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('images[]', files[i]);
}
try {
const response = await fetch('upload_images.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
uploadStatus.textContent = `✅ Successfully uploaded ${result.uploaded.length} file(s)`;
uploadStatus.style.color = '#28a745';
// Refresh the image list
setTimeout(() => {
loadImages();
uploadStatus.textContent = '';
}, 2000);
} else {
uploadStatus.textContent = `❌ Upload failed: ${result.error}`;
uploadStatus.style.color = '#dc3545';
}
} catch (error) {
console.error('Upload error:', error);
uploadStatus.textContent = '❌ Upload failed: Network error';
uploadStatus.style.color = '#dc3545';
}
// Clear the file input
document.getElementById('fileInput').value = '';
}
</script>
</body>
</html>