<!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 - Dark UI</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #121212;
color: #eee;
height: 100vh;
display: flex;
flex-direction: column;
}
/* Top bar */
.topbar {
display: flex;
justify-content: space-around;
padding: 0.5rem 0;
background: #1e1e1e;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
font-size: 1.5rem;
z-index: 10;
}
.topbar button {
flex: 1;
background: none;
border: none;
font-size: 1.6rem;
cursor: pointer;
padding: 0.8rem 0;
color: #eee;
transition: background 0.2s, color 0.2s;
}
.topbar button:hover {
background: #2c2c2c;
color: #4fc3f7;
}
/* Slide-down panels */
.panel {
max-height: 0;
overflow: hidden;
background: #1b1b1b;
transition: max-height 0.4s ease;
}
.panel.open {
max-height: 260px;
}
/* Gallery */
.gallery-track {
display: flex;
gap: 0.75rem;
overflow-x: auto;
padding: 0.75rem 1rem;
}
.gallery-track::-webkit-scrollbar {
height: 8px;
}
.gallery-track::-webkit-scrollbar-track {
background: #2c2c2c;
border-radius: 4px;
}
.gallery-track::-webkit-scrollbar-thumb {
background: #4fc3f7;
border-radius: 4px;
}
.image-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
background: #2c2c2c;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
min-width: 120px;
flex-shrink: 0;
}
.image-item:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(79, 195, 247, 0.3);
border-color: #4fc3f7;
}
.image-item.active {
border-color: #4fc3f7;
background: #333;
}
.image-item img {
width: 100px;
height: 70px;
object-fit: contain;
border-radius: 8px;
margin-bottom: 8px;
background: #1e1e1e;
border: 1px solid #333;
}
.image-name {
font-weight: 500;
color: #eee;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
max-width: 100px;
text-align: center;
}
/* Settings row */
.settings-track {
display: flex;
gap: 1rem;
overflow-x: auto;
padding: 0.5rem 1rem;
}
.settings-track::-webkit-scrollbar {
height: 8px;
}
.settings-track::-webkit-scrollbar-track {
background: #2c2c2c;
border-radius: 4px;
}
.settings-track::-webkit-scrollbar-thumb {
background: #4fc3f7;
border-radius: 4px;
}
.stepper {
flex: 0 0 auto;
background: #2c2c2c;
border-radius: 0.6rem;
padding: 0.5rem;
text-align: center;
color: #eee;
min-width: 110px;
}
.stepper label {
display: block;
font-size: 0.8rem;
margin-bottom: 0.3rem;
color: #bbb;
}
.stepper-controls {
display: flex;
align-items: center;
justify-content: space-between;
background: #1e1e1e;
border-radius: 0.5rem;
overflow: hidden;
}
.stepper button {
flex: 0 0 30%;
background: #333;
border: none;
color: #eee;
font-size: 1.2rem;
cursor: pointer;
padding: 0.3rem 0;
transition: background 0.2s;
}
.stepper button:hover {
background: #4fc3f7;
color: #000;
}
.stepper input {
flex: 1;
background: #1e1e1e;
border: none;
color: #eee;
text-align: center;
font-size: 1rem;
width: 40px;
outline: none;
}
/* Action panels */
.actions-track {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
padding: 0.8rem 1rem;
justify-content: center;
}
.actions-track button {
background: #2c2c2c;
border: none;
border-radius: 0.5rem;
padding: 0.6rem 1rem;
font-size: 0.9rem;
color: #eee;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.actions-track button:hover {
background: #4fc3f7;
color: #000;
}
/* Content area */
.content {
flex: 1;
background: linear-gradient(135deg, #212121, #2c2c2c);
padding: 1.5rem;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
overflow: auto;
display: flex;
flex-direction: column;
}
.editor-header {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #333;
}
.editor-title {
font-size: 1.3rem;
color: #4fc3f7;
margin-bottom: 10px;
}
.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: #1e1e1e;
border-radius: 8px;
border: 2px dashed #333;
}
.tile-tag {
background: linear-gradient(135deg, #4fc3f7 0%, #29b6f6 100%);
color: #000;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
font-weight: 500;
}
.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: #1e1e1e;
border-radius: 10px;
padding: 20px;
position: relative;
overflow: auto;
min-height: 400px;
touch-action: none; /* Prevent default touch behaviors */
}
.tile-grid-container {
position: relative;
display: inline-block;
border: 2px solid #333;
border-radius: 8px;
background: #000;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
image-rendering: pixelated;
image-rendering: crisp-edges;
transform-origin: top left; /* Ensure zoom originates from top-left */
}
.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 #FF0000; /* Bright red */
border-bottom: 1px solid #FF0000; /* Bright red */
box-sizing: border-box;
}
.tile-cell:nth-child(-n + var(--grid-cols)) {
border-top: 1px solid #FF0000; /* Bright red */
}
.tile-cell:nth-child(var(--grid-cols) + 1) ~ .tile-cell {
border-top: 1px solid #FF0000; /* Bright red */
}
.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); /* Bright red with transparency */
outline: 2px solid #FF0000; /* Bright red outline */
box-sizing: border-box;
}
.tile-info {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.9);
color: #4fc3f7;
padding: 10px;
border-radius: 5px;
font-size: 12px;
display: none;
z-index: 10;
border: 1px solid #333;
}
/* Modal styles */
.modal {
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);
}
.modal-content {
position: relative;
max-width: 90vw;
max-height: 90vh;
background: #1e1e1e;
border-radius: 15px;
padding: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
border: 1px solid #333;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #333;
}
.modal-title {
font-size: 1.2rem;
font-weight: 600;
color: #4fc3f7;
}
.modal-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;
}
.modal-close:hover {
background: #c82333;
}
.json-output pre {
background: #121212;
padding: 15px;
border-radius: 5px;
border: 1px solid #333;
font-size: 12px;
overflow: auto;
max-height: 60vh;
margin-bottom: 15px;
color: #eee;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.loading::before {
content: '';
display: inline-block;
width: 30px;
height: 30px;
border: 3px solid #333;
border-top: 3px solid #4fc3f7;
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;
}
/* Preview canvas styles */
.preview-canvas-container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
max-height: 70vh;
overflow: auto;
border-radius: 8px;
background: #121212;
}
.preview-canvas {
border: 2px solid #333;
border-radius: 5px;
background: #000;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.preview-info {
margin-top: 10px;
padding: 10px;
background: #121212;
border-radius: 8px;
font-size: 12px;
color: #666;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
/* File input styling */
input[type="file"] {
display: none;
}
/* Mobile responsive */
@media (max-width: 768px) {
.topbar {
font-size: 1.2rem;
}
.topbar button {
padding: 0.6rem 0;
}
.content {
padding: 1rem;
}
.canvas-container {
padding: 10px;
min-height: 300px;
}
.tile-grid-container {
max-width: calc(100vw - 60px);
max-height: calc(100vh - 400px);
}
}
</style>
</head>
<body>
<div class="topbar">
<button id="settingsBtn" title="Settings">โ๏ธ</button>
<button id="imagesBtn" title="Images">๐ผ๏ธ</button>
<button id="saveBtn" title="Save">๐พ</button>
<button id="refreshBtn" title="Refresh" onclick="updateGrid()">๐</button>
<button id="moreBtn" title="More">โฎ</button>
</div>
<!-- Settings panel -->
<div class="panel" id="settingsPanel">
<div class="settings-track">
<div class="stepper">
<label>Tile Width</label>
<div class="stepper-controls">
<button onclick="changeValue('tileWidth', -8)">-</button>
<input type="number" id="tileWidth" value="32" min="8" step="8">
<button onclick="changeValue('tileWidth', 8)">+</button>
</div>
</div>
<div class="stepper">
<label>Tile Height</label>
<div class="stepper-controls">
<button onclick="changeValue('tileHeight', -8)">-</button>
<input type="number" id="tileHeight" value="32" min="8" step="8">
<button onclick="changeValue('tileHeight', 8)">+</button>
</div>
</div>
<div class="stepper">
<label>X Offset</label>
<div class="stepper-controls">
<button onclick="changeValue('xOffset', -1)">-</button>
<input type="number" id="xOffset" value="0" min="-50" max="50">
<button onclick="changeValue('xOffset', 1)">+</button>
</div>
</div>
<div class="stepper">
<label>Y Offset</label>
<div class="stepper-controls">
<button onclick="changeValue('yOffset', -1)">-</button>
<input type="number" id="yOffset" value="0" min="-50" max="50">
<button onclick="changeValue('yOffset', 1)">+</button>
</div>
</div>
<div class="stepper">
<label>V Spacing</label>
<div class="stepper-controls">
<button onclick="changeValue('verticalSpacing', -1)">-</button>
<input type="number" id="verticalSpacing" value="0" min="0" max="50">
<button onclick="changeValue('verticalSpacing', 1)">+</button>
</div>
</div>
<div class="stepper">
<label>H Spacing</label>
<div class="stepper-controls">
<button onclick="changeValue('horizontalSpacing', -1)">-</button>
<input type="number" id="horizontalSpacing" value="0" min="0" max="50">
<button onclick="changeValue('horizontalSpacing', 1)">+</button>
</div>
</div>
</div>
</div>
<!-- Gallery panel -->
<div class="panel" id="gallery">
<div class="gallery-track" id="imageGallery">
<div class="loading">Loading images...</div>
</div>
</div>
<!-- Save actions panel -->
<div class="panel" id="savePanel">
<div class="actions-track">
<input type="file" id="fileInput" accept="image/*" multiple>
<button onclick="document.getElementById('fileInput').click()">๐ Add Images</button>
<button onclick="generateJSON()">๐ Generate JSON</button>
<button onclick="downloadTile()">๐พ Download Tile</button>
<button onclick="downloadJSON()">๐พ Download JSON</button>
</div>
</div>
<!-- More actions panel -->
<div class="panel" id="actionsPanel">
<div class="actions-track">
<button onclick="clearSelection()">๐งน Clear Selection</button>
<button onclick="copyJSON()">๐ Copy JSON</button>
<button onclick="showImagePreview(currentImagePath)">๐ Preview Image</button>
</div>
</div>
<div class="content">
<div class="editor-header">
<div class="editor-title" id="editorTitle">Tile Sheet Editor - Select an image to start</div>
<div class="selected-tiles-section">
<h4 style="color: #4fc3f7; margin-bottom: 8px;">Selected Tiles:</h4>
<div class="selected-tiles-list" id="selectedTilesList">
<span style="color: #666; font-style: italic;">Click tiles to select them...</span>
</div>
<div id="uploadStatus" style="font-size: 12px; color: #666; margin-top: 8px;"></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>
<!-- Image preview modal -->
<div class="modal" id="imagePreview">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title" id="previewTitle">Image Preview</div>
<button class="modal-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>
<!-- JSON output modal -->
<div class="modal" id="jsonOutput">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">Generated JSON</div>
<button class="modal-close" onclick="closeJSONOutput()">ร</button>
</div>
<pre id="jsonContent"></pre>
<div class="actions-track">
<button onclick="copyJSON()">๐ Copy to Clipboard</button>
<button onclick="downloadJSON()">๐พ Download JSON</button>
</div>
</div>
</div>
<script>
// UI State Management
const settingsBtn = document.getElementById('settingsBtn');
const imagesBtn = document.getElementById('imagesBtn');
const saveBtn = document.getElementById('saveBtn');
const moreBtn = document.getElementById('moreBtn');
const settingsPanel = document.getElementById('settingsPanel');
const gallery = document.getElementById('gallery');
const savePanel = document.getElementById('savePanel');
const actionsPanel = document.getElementById('actionsPanel');
function closeAll() {
settingsPanel.classList.remove('open');
gallery.classList.remove('open');
savePanel.classList.remove('open');
actionsPanel.classList.remove('open');
}
settingsBtn.addEventListener('click', () => {
const isOpen = settingsPanel.classList.contains('open');
closeAll();
if (!isOpen) settingsPanel.classList.add('open');
});
imagesBtn.addEventListener('click', () => {
const isOpen = gallery.classList.contains('open');
closeAll();
if (!isOpen) gallery.classList.add('open');
});
saveBtn.addEventListener('click', () => {
const isOpen = savePanel.classList.contains('open');
closeAll();
if (!isOpen) savePanel.classList.add('open');
});
moreBtn.addEventListener('click', () => {
const isOpen = actionsPanel.classList.contains('open');
closeAll();
if (!isOpen) actionsPanel.classList.add('open');
});
// Tile Editor Core Variables
let currentImage = null;
let currentImagePath = '';
let selectedTiles = new Set();
let lastSelectedTile = { x: -1, y: -1 };
let gridCols = 0;
let gridRows = 0;
// Zoom and Pan Variables for tile-grid-container
let gridZoom = 1;
let gridOffsetX = 0;
let gridOffsetY = 0;
let isGridDragging = false;
let gridLastMouseX = 0;
let gridLastMouseY = 0;
const tileGridContainer = document.getElementById('tileGridContainer');
// Initialize zoom and pan
function initGridZoomPan() {
tileGridContainer.style.transform = `scale(${gridZoom}) translate(${gridOffsetX}px, ${gridOffsetY}px)`;
// Mouse wheel for zooming
tileGridContainer.addEventListener('wheel', function(e) {
e.preventDefault();
const rect = tileGridContainer.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.5, Math.min(5, gridZoom * zoomFactor)); // Limit zoom: 0.5x to 5x
// Adjust offsets to zoom towards mouse position
gridOffsetX = mouseX / gridZoom - (mouseX / newZoom - gridOffsetX);
gridOffsetY = mouseY / gridZoom - (mouseY / newZoom - gridOffsetY);
gridZoom = newZoom;
updateGridTransform();
});
// Mouse drag for panning
tileGridContainer.addEventListener('mousedown', function(e) {
if (e.button === 2) return; // Ignore right-click
isGridDragging = true;
gridLastMouseX = e.clientX;
gridLastMouseY = e.clientY;
tileGridContainer.style.cursor = 'grabbing';
});
tileGridContainer.addEventListener('mousemove', function(e) {
if (isGridDragging) {
const deltaX = (e.clientX - gridLastMouseX) / gridZoom;
const deltaY = (e.clientY - gridLastMouseY) / gridZoom;
gridOffsetX += deltaX;
gridOffsetY += deltaY;
gridLastMouseX = e.clientX;
gridLastMouseY = e.clientY;
updateGridTransform();
}
});
tileGridContainer.addEventListener('mouseup', function() {
isGridDragging = false;
tileGridContainer.style.cursor = 'crosshair';
});
tileGridContainer.addEventListener('mouseleave', function() {
isGridDragging = false;
tileGridContainer.style.cursor = 'crosshair';
});
// Touch events for pinch-to-zoom and panning
let initialDistance = 0;
let initialZoom = 1;
let initialOffsetX = 0;
let initialOffsetY = 0;
tileGridContainer.addEventListener('touchstart', function(e) {
e.preventDefault();
if (e.touches.length === 1) {
isGridDragging = true;
gridLastMouseX = e.touches[0].clientX;
gridLastMouseY = e.touches[0].clientY;
} else if (e.touches.length === 2) {
isGridDragging = false;
const touch1 = e.touches[0];
const touch2 = e.touches[1];
initialDistance = Math.hypot(touch1.clientX - touch2.clientX, touch1.clientY - touch2.clientY);
initialZoom = gridZoom;
initialOffsetX = gridOffsetX;
initialOffsetY = gridOffsetY;
}
});
tileGridContainer.addEventListener('touchmove', function(e) {
e.preventDefault();
if (e.touches.length === 1 && isGridDragging) {
const deltaX = (e.touches[0].clientX - gridLastMouseX) / gridZoom;
const deltaY = (e.touches[0].clientY - gridLastMouseY) / gridZoom;
gridOffsetX += deltaX;
gridOffsetY += deltaY;
gridLastMouseX = e.touches[0].clientX;
gridLastMouseY = e.touches[0].clientY;
updateGridTransform();
} else if (e.touches.length === 2) {
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = Math.hypot(touch1.clientX - touch2.clientX, touch1.clientY - touch2.clientY);
const zoomFactor = currentDistance / initialDistance;
const newZoom = Math.max(0.5, Math.min(5, initialZoom * zoomFactor));
// Adjust offsets to zoom towards the midpoint of the pinch
const midX = (touch1.clientX + touch2.clientX) / 2 - tileGridContainer.getBoundingClientRect().left;
const midY = (touch1.clientY + touch2.clientY) / 2 - tileGridContainer.getBoundingClientRect().top;
gridOffsetX = midX / initialZoom - (midX / newZoom - initialOffsetX);
gridOffsetY = midY / initialZoom - (midY / newZoom - initialOffsetY);
gridZoom = newZoom;
updateGridTransform();
}
});
tileGridContainer.addEventListener('touchend', function(e) {
if (e.touches.length < 2) {
isGridDragging = false;
tileGridContainer.style.cursor = 'crosshair';
}
});
}
function updateGridTransform() {
tileGridContainer.style.transform = `scale(${gridZoom}) translate(${gridOffsetX}px, ${gridOffsetY}px)`;
}
// Stepper functionality
function changeValue(inputId, change) {
const input = document.getElementById(inputId);
const currentValue = parseInt(input.value) || 0;
const min = parseInt(input.min) || -Infinity;
const max = parseInt(input.max) || Infinity;
const step = parseInt(input.step) || 1;
let newValue = currentValue + change;
// Apply step constraints for tile dimensions
if (inputId === 'tileWidth' || inputId === 'tileHeight') {
newValue = Math.round(newValue / step) * step;
}
// Apply min/max constraints
newValue = Math.max(min, Math.min(max, newValue));
input.value = newValue;
updateGrid();
}
// Load images from PHP script
async function loadImages() {
try {
const response = await fetch('get_images.php');
const result = await response.json();
const imageGallery = document.getElementById('imageGallery');
// Handle error response
if (result.error) {
imageGallery.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) {
imageGallery.innerHTML = '<div class="no-images">No images found in the images folder</div>';
return;
}
imageGallery.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: 100px;
height: 70px;
background: #333;
border: 1px solid #555;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #666;
margin-bottom: 8px;
border-radius: 8px;
`;
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);
});
imageGallery.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('imageGallery').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';
// Clear selection when loading new image
selectedTiles.clear();
updateSelectedTilesList();
// Reset zoom and pan
gridZoom = 1;
gridOffsetX = 0;
gridOffsetY = 0;
updateGridTransform();
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);
// 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 (hSpacing === 0) {
gridCols = Math.floor((currentImage.width - xOffset) / tileWidth);
}
if (vSpacing === 0) {
gridRows = Math.floor((currentImage.height - yOffset) / tileHeight);
}
// Calculate total tiled area
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`;
overlay.style.setProperty('--grid-cols', gridCols);
overlay.style.setProperty('--grid-rows', gridRows);
// 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}`;
if (selectedTiles.has(`${col},${row}`)) {
cell.classList.add('selected');
}
cell.addEventListener('click', function(e) {
e.stopPropagation();
// Adjust click coordinates for zoom and pan
const rect = tileGridContainer.getBoundingClientRect();
const clickX = (e.clientX - rect.left - gridOffsetX * gridZoom) / gridZoom;
const clickY = (e.clientY - rect.top - gridOffsetY * gridZoom) / gridZoom;
const adjustedCol = Math.floor((clickX - xOffset) / (tileWidth + hSpacing));
const adjustedRow = Math.floor((clickY - yOffset) / (tileHeight + vSpacing));
if (adjustedCol >= 0 && adjustedCol < gridCols && adjustedRow >= 0 && adjustedRow < gridRows) {
toggleTile(adjustedCol, adjustedRow, `${adjustedCol},${adjustedRow}`);
}
});
overlay.appendChild(cell);
}
}
// Update transform to maintain zoom and pan
updateGridTransform();
}
function toggleTile(col, row, tileKey) {
if (selectedTiles.has(tileKey)) {
selectedTiles.delete(tileKey);
} else {
selectedTiles.add(tileKey);
}
lastSelectedTile.x = col;
lastSelectedTile.y = row;
updateSelectedTilesList();
updateGrid();
// 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>
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: #666; 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();
closeAll();
}
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 = 'flex';
closeAll();
}
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 closeJSONOutput() {
document.getElementById('jsonOutput').style.display = 'none';
}
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);
const actualTileX = xOffset + lastSelectedTile.x * (tileWidth + hSpacing);
const actualTileY = yOffset + lastSelectedTile.y * (tileHeight + vSpacing);
const tileCanvas = document.createElement('canvas');
const tileCtx = tileCanvas.getContext('2d');
tileCanvas.width = tileWidth;
tileCanvas.height = tileHeight;
tileCtx.drawImage(
currentImage,
actualTileX, actualTileY, tileWidth, tileHeight,
0, 0, tileWidth, tileHeight
);
const link = document.createElement('a');
link.download = `tile_${lastSelectedTile.x}_${lastSelectedTile.y}.png`;
link.href = tileCanvas.toDataURL();
link.click();
closeAll();
}
// 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) {
if (!imagePath) return;
const img = new Image();
img.onload = function() {
previewImage = img;
previewZoom = 1;
previewOffsetX = 0;
previewOffsetY = 0;
const maxWidth = window.innerWidth * 0.7;
const maxHeight = window.innerHeight * 0.6;
let displayWidth = img.width;
let displayHeight = img.height;
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;
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 and drag to pan โข Scroll to zoom</span>
`;
drawPreview();
document.getElementById('imagePreview').style.display = 'flex';
closeAll();
};
img.onerror = function() {
alert('Failed to load preview for: ' + imagePath.split('/').pop());
};
img.src = imagePath;
}
function drawPreview() {
if (!previewImage) return;
previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height);
previewCtx.save();
previewCtx.translate(previewOffsetX, previewOffsetY);
previewCtx.scale(previewZoom, previewZoom);
previewCtx.imageSmoothingEnabled = false;
previewCtx.drawImage(previewImage, 0, 0);
previewCtx.restore();
}
function closeImagePreview() {
document.getElementById('imagePreview').style.display = 'none';
previewImage = null;
}
// Preview canvas events
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));
previewOffsetX = mouseX - (mouseX - previewOffsetX) * (newZoom / previewZoom);
previewOffsetY = mouseY - (mouseY - previewOffsetY) * (newZoom / previewZoom);
previewZoom = newZoom;
document.getElementById('previewInfo').innerHTML = `
<span>Dimensions: ${previewImage.width} ร ${previewImage.height}px</span>
<span>Zoom: ${Math.round(previewZoom * 100)}%</span>
<span>Click and drag to pan โข Scroll to zoom</span>
`;
drawPreview();
});
previewCanvas.addEventListener('mousedown', function(e) {
if (e.button === 2) {
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';
// File upload handling
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 = '#4fc3f7';
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 = '#4fc3f7';
setTimeout(() => {
loadImages();
uploadStatus.textContent = '';
}, 2000);
} else {
uploadStatus.textContent = `โ Upload failed: ${result.error}`;
uploadStatus.style.color = '#f44336';
}
} catch (error) {
uploadStatus.textContent = 'โ Upload failed: Network error';
uploadStatus.style.color = '#f44336';
}
document.getElementById('fileInput').value = '';
}
// Event listeners
document.getElementById('tileGridContainer').addEventListener('mouseleave', function() {
document.getElementById('tileInfo').style.display = 'none';
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeImagePreview();
closeJSONOutput();
closeAll();
}
});
document.getElementById('imagePreview').addEventListener('click', function(e) {
if (e.target === this) closeImagePreview();
});
document.getElementById('jsonOutput').addEventListener('click', function(e) {
if (e.target === this) closeJSONOutput();
});
// Input event listeners
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);
// Initialize
loadImages();
initGridZoomPan();
</script>
</body>
</html>