<!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;
}
.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;
}
}
canvas {
border: 2px solid #ddd;
border-radius: 8px;
background: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
cursor: crosshair;
max-width: 100%;
height: auto;
}
@media (max-width: 768px) {
canvas {
max-width: calc(100vw - 60px);
max-height: calc(100vh - 400px);
}
}
.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); }
}
.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;
}
.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;
}
</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 Sample Images
</button>
<div id="uploadStatus" style="font-size: 12px; color: #666; text-align: center;"></div>
</div>
<div class="image-list" id="imageList">
<div class="no-images">Add some tile sheet images to get started</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="1">
</div>
<div class="control-group">
<label>Tile Height:</label>
<input type="number" id="tileHeight" value="32" min="1">
</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>
<div class="control-group">
<label>Zoom:</label>
<input type="number" id="zoomLevel" value="2" min="1" max="8">
</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">
<canvas id="canvas"></canvas>
<div class="tile-info" id="tileInfo"></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 canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let selectedTiles = new Set();
let lastSelectedTile = { x: -1, y: -1 };
let gridCols = 0;
let gridRows = 0;
// Load images from PHP script with enhanced debugging
async function loadImages() {
const imageList = document.getElementById('imageList');
try {
console.log('Attempting to fetch from get_images.php...');
const response = await fetch('get_images.php');
console.log('Response status:', response.status);
console.log('Response headers:', response.headers);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const responseText = await response.text();
console.log('Raw response:', responseText);
let result;
try {
result = JSON.parse(responseText);
} catch (parseError) {
console.error('JSON parse error:', parseError);
throw new Error(`Invalid JSON response: ${responseText.substring(0, 100)}...`);
}
console.log('Parsed result:', result);
// Handle error response
if (result.error) {
imageList.innerHTML = `
<div class="no-images">
<strong>PHP 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><br>
<button class="btn" onclick="debugPHP()" style="margin-top: 10px;">Debug PHP</button>
</div>
`;
return false;
}
// Handle array of images
const images = Array.isArray(result) ? result : (result.images || []);
console.log('Found images:', images);
if (images.length === 0) {
imageList.innerHTML = `
<div class="no-images">
No images found in the images folder<br>
<small>PHP script working but no images detected</small><br>
<button class="btn" onclick="debugPHP()" style="margin-top: 10px;">Debug PHP</button>
</div>
`;
return false;
}
imageList.innerHTML = '';
let successfulImages = 0;
images.forEach((imagePath, index) => {
console.log(`Processing image ${index + 1}/${images.length}:`, imagePath);
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(),
imagePath.replace('images/', '../images/')
];
console.log('Trying paths for', imagePath, ':', possiblePaths);
let pathIndex = 0;
function tryNextPath() {
if (pathIndex < possiblePaths.length) {
const currentPath = possiblePaths[pathIndex];
console.log(`Trying path ${pathIndex + 1}/${possiblePaths.length}:`, currentPath);
img.src = currentPath;
pathIndex++;
} else {
console.error('All paths failed for:', imagePath);
// All paths failed, show placeholder
img.style.display = 'none';
const placeholder = document.createElement('div');
placeholder.style.cssText = `
width: 60px;
height: 60px;
background: #ffebee;
border: 1px solid #f44336;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #c62828;
margin-right: 10px;
border-radius: 5px;
`;
placeholder.textContent = 'Failed';
placeholder.title = `Failed to load: ${imagePath}`;
item.insertBefore(placeholder, img);
}
}
img.onerror = function() {
console.error('Failed to load:', img.src);
tryNextPath();
};
img.onload = function() {
console.log('Successfully loaded:', img.src);
successfulImages++;
};
const nameDiv = document.createElement('div');
nameDiv.className = 'image-name';
nameDiv.textContent = imagePath.split('/').pop();
nameDiv.title = imagePath; // Show full path on hover
item.appendChild(img);
item.appendChild(nameDiv);
item.addEventListener('click', () => {
document.querySelectorAll('.image-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
loadImageFromURL(img.src, imagePath);
});
imageList.appendChild(item);
// Start trying paths
tryNextPath();
});
// Auto-load first image after a delay
setTimeout(() => {
console.log(`Loaded ${successfulImages}/${images.length} images successfully`);
const firstWorkingImg = document.querySelector('.image-item img[src]:not([style*="display: none"])');
if (firstWorkingImg && firstWorkingImg.complete && firstWorkingImg.naturalWidth > 0) {
firstWorkingImg.closest('.image-item').click();
console.log('Auto-loaded first working image');
}
}, 2000);
return true;
} catch (error) {
console.error('Error loading images:', error);
imageList.innerHTML = `
<div class="no-images">
<strong>Connection Error:</strong> ${error.message}<br>
<small>Cannot reach get_images.php</small><br>
<button class="btn" onclick="debugPHP()" style="margin-top: 10px;">Debug Connection</button>
</div>
`;
return false;
}
}
// Debug function to help troubleshoot PHP connection
async function debugPHP() {
console.log('Starting PHP debug...');
try {
const response = await fetch('get_images.php?debug=1');
const result = await response.text();
console.log('Debug response:', result);
// Show debug info in a popup
const debugWindow = window.open('', 'debug', 'width=800,height=600');
debugWindow.document.write(`
<html>
<head><title>PHP Debug Info</title></head>
<body style="font-family: monospace; padding: 20px;">
<h2>PHP Debug Response</h2>
<pre style="background: #f5f5f5; padding: 15px; border-radius: 5px; overflow: auto;">
${result}
</pre>
<br>
<button onclick="window.close()">Close</button>
</body>
</html>
`);
} catch (error) {
alert('Debug failed: ' + error.message);
}
}
function loadImageFromURL(url, filename) {
const img = new Image();
img.onload = function() {
currentImage = img;
currentImagePath = filename;
document.getElementById('editorTitle').textContent = `Editing: ${filename}`;
const zoom = parseInt(document.getElementById('zoomLevel').value);
canvas.width = img.width * zoom;
canvas.height = img.height * zoom;
// Clear selection when loading new image
selectedTiles.clear();
updateSelectedTilesList();
drawImage();
updateGrid();
};
img.src = url;
}
function drawImage() {
if (!currentImage) return;
const zoom = parseInt(document.getElementById('zoomLevel').value);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw the image scaled
ctx.imageSmoothingEnabled = false;
ctx.drawImage(currentImage, 0, 0, currentImage.width * zoom, currentImage.height * zoom);
}
function drawGrid() {
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);
const zoom = parseInt(document.getElementById('zoomLevel').value);
// Calculate grid dimensions properly accounting for spacing and offset
const availableWidth = currentImage.width - xOffset;
const availableHeight = currentImage.height - yOffset;
gridCols = Math.floor(availableWidth / (tileWidth + hSpacing));
gridRows = Math.floor(availableHeight / (tileHeight + vSpacing));
// If there's no horizontal spacing, we can fit one more column potentially
if (hSpacing === 0 && availableWidth % tileWidth !== 0) {
gridCols = Math.floor(availableWidth / tileWidth);
}
if (vSpacing === 0 && availableHeight % tileHeight !== 0) {
gridRows = Math.floor(availableHeight / tileHeight);
}
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 1;
// Draw grid with proper spacing and offset
for (let row = 0; row < gridRows; row++) {
for (let col = 0; col < gridCols; col++) {
const x = (xOffset + col * (tileWidth + hSpacing)) * zoom;
const y = (yOffset + row * (tileHeight + vSpacing)) * zoom;
const w = tileWidth * zoom;
const h = tileHeight * zoom;
ctx.strokeRect(x, y, w, h);
}
}
// Highlight selected tiles
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 2;
selectedTiles.forEach(tileKey => {
const [col, row] = tileKey.split(',').map(Number);
const x = (xOffset + col * (tileWidth + hSpacing)) * zoom;
const y = (yOffset + row * (tileHeight + vSpacing)) * zoom;
const w = tileWidth * zoom;
const h = tileHeight * zoom;
ctx.fillRect(x, y, w, h);
ctx.strokeRect(x, y, w, h);
});
}
function updateGrid() {
drawImage();
drawGrid();
}
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();
}
canvas.addEventListener('click', function(e) {
if (!currentImage) return;
const rect = canvas.getBoundingClientRect();
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
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 zoom = parseInt(document.getElementById('zoomLevel').value);
// Convert canvas coordinates to image coordinates
const imageX = canvasX / zoom;
const imageY = canvasY / zoom;
// Adjust for offset
const adjustedX = imageX - xOffset;
const adjustedY = imageY - yOffset;
// Check if click is within the grid area
if (adjustedX < 0 || adjustedY < 0) return;
// Calculate which tile was clicked
const tileCol = Math.floor(adjustedX / (tileWidth + hSpacing));
const tileRow = Math.floor(adjustedY / (tileHeight + vSpacing));
// Check if the click is within valid grid bounds
if (tileCol >= gridCols || tileRow >= gridRows || tileCol < 0 || tileRow < 0) {
return;
}
// Check if click is within the actual tile area (not in spacing)
const tileStartX = tileCol * (tileWidth + hSpacing);
const tileStartY = tileRow * (tileHeight + vSpacing);
const relativeX = adjustedX - tileStartX;
const relativeY = adjustedY - tileStartY;
if (relativeX >= tileWidth || relativeY >= tileHeight || relativeX < 0 || relativeY < 0) {
return; // Click was in spacing area
}
const tileKey = `${tileCol},${tileRow}`;
if (selectedTiles.has(tileKey)) {
selectedTiles.delete(tileKey);
} else {
selectedTiles.add(tileKey);
}
lastSelectedTile.x = tileCol;
lastSelectedTile.y = tileRow;
updateSelectedTilesList();
updateGrid();
// Calculate actual pixel position in the original image
const actualTileX = xOffset + tileCol * (tileWidth + hSpacing);
const actualTileY = yOffset + tileRow * (tileHeight + vSpacing);
const tileInfo = document.getElementById('tileInfo');
tileInfo.innerHTML = `
Tile: (${tileCol}, ${tileRow})<br>
Position: (${actualTileX}px, ${actualTileY}px)<br>
Size: ${tileWidth}×${tileHeight}px<br>
Spacing: H${hSpacing}px V${vSpacing}px<br>
Status: ${selectedTiles.has(tileKey) ? 'Selected' : 'Unselected'}
`;
tileInfo.style.display = 'block';
});
canvas.addEventListener('mousemove', function(e) {
if (!currentImage) return;
const rect = canvas.getBoundingClientRect();
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
const zoom = parseInt(document.getElementById('zoomLevel').value);
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);
// Convert to image coordinates
const imageX = canvasX / zoom;
const imageY = canvasY / zoom;
const adjustedX = imageX - xOffset;
const adjustedY = imageY - yOffset;
if (adjustedX >= 0 && adjustedY >= 0) {
const tileCol = Math.floor(adjustedX / (tileWidth + hSpacing));
const tileRow = Math.floor(adjustedY / (tileHeight + vSpacing));
if (tileCol < gridCols && tileRow < gridRows && tileCol >= 0 && tileRow >= 0) {
// Check if we're in the tile area, not spacing
const tileStartX = tileCol * (tileWidth + hSpacing);
const tileStartY = tileRow * (tileHeight + vSpacing);
const relativeX = adjustedX - tileStartX;
const relativeY = adjustedY - tileStartY;
if (relativeX < tileWidth && relativeY < tileHeight && relativeX >= 0 && relativeY >= 0) {
canvas.style.cursor = 'pointer';
} else {
canvas.style.cursor = 'crosshair';
}
} else {
canvas.style.cursor = 'crosshair';
}
} else {
canvas.style.cursor = 'crosshair';
}
});
canvas.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: currentImagePath,
image_dimensions: {
width: currentImage.width,
height: currentImage.height